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

# Pest Testing 4

## Documentation

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

## Basic Usage

### Creating Tests

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

### Test Organization

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

### Basic Test Structure

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

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

### Running Tests

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

## Assertions

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

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

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

## Mocking

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

## Datasets

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

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

## Pest 4 Features

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

### Browser Test Example

Browser tests run in real browsers for full integration testing:

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

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

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

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

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

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

### Smoke Testing

Quickly validate multiple pages have no JavaScript errors:

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

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

### Visual Regression Testing

Capture and compare screenshots to detect visual changes.

### Test Sharding

Split tests across parallel processes for faster CI runs.

### Architecture Testing

Pest 4 includes architecture testing (from Pest 3):

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

## Common Pitfalls

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


server: laravel-boost

tool: application-info

type: text

database_engine: sqlite

roster_name: FORTIFY

package_name: laravel/fortify

roster_name: LARAVEL

package_name: laravel/framework

roster_name: PROMPTS

package_name: laravel/prompts

package_name: livewire/flux

roster_name: LIVEWIRE

package_name: livewire/livewire

roster_name: BOOST

package_name: laravel/boost

roster_name: MCP

package_name: laravel/mcp

roster_name: PAIL

package_name: laravel/pail

roster_name: PINT

package_name: laravel/pint

roster_name: SAIL

package_name: laravel/sail

roster_name: PEST

package_name: pestphp/pest

roster_name: PHPUNIT

package_name: phpunit/phpunit

roster_name: TAILWINDCSS

package_name: tailwindcss


message: The app is on Laravel 12.51, Livewire 4.1, Flux 2.12, Pest 4.3, matching the project guidance. I’m checking the current diff and the generated test file now so the next patch is scoped to the discounts feature only.

phase: commentary


type: message

role: assistant

text: The app is on Laravel 12.51, Livewire 4.1, Flux 2.12, Pest 4.3, matching the project guidance. I’m checking the current diff and the generated test file now so the next patch is scoped to the discounts feature only.


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


cmd: sed -n '1,260p' app/Livewire/Admin/Discounts/Index.php


cmd: sed -n '1,360p' app/Livewire/Admin/Discounts/Form.php


cmd: sed -n '1,220p' tests/Feature/Admin/CustomerManagementTest.php


cmd: sed -n '1,220p' tests/Feature/Admin/OrderManagementTest.php


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

DiscountManagementTest.php

type: read

aggregated_output:
<?php

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
use App\Models\Collection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminDiscountManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

test('admin discount routes require authentication and render store scoped discounts', function (): void {
    $store = adminDiscountManagementStore();
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SAVE20',
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 20,
        'usage_count' => 3,
        'usage_limit' => 100,
    ]);
    Discount::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'code' => 'OTHER20',
    ]);

    $this->get('/admin/discounts')->assertRedirect('/admin/login');

    $this->actingAs(adminDiscountManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/discounts')
        ->assertSuccessful()
        ->assertSee('SAVE20')
        ->assertSee('20%')
        ->assertSee('3 / 100')
        ->assertDontSee('OTHER20');

    $this->actingAs(adminDiscountManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/discounts/create')
        ->assertSuccessful()
        ->assertSee('Create discount');
});

test('admin discount index filters by code and effective status', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'ACTIVE10',
    ]);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SOON10',
        'starts_at' => now()->addWeek(),
    ]);

    Livewire::actingAs($user)
        ->test(AdminDiscountsIndex::class)
        ->assertSee('ACTIVE10')
        ->assertSee('SOON10')
        ->set('search', 'active')
        ->assertSee('ACTIVE10')
        ->assertDontSee('SOON10')
        ->set('search', '')
        ->set('statusFilter', 'scheduled')
        ->assertSee('SOON10')
        ->assertDontSee('ACTIVE10');
});

test('admin discount form creates discounts with eligibility rules', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    $product = Product::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
    $collection = Collection::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class)
        ->set('type', 'code')
        ->set('code', 'VIP25')
        ->set('valueType', 'fixed')
        ->set('valueAmount', '5.00')
        ->set('minimumPurchaseAmount', '25.00')
        ->set('usageLimit', '100')
        ->set('onePerCustomer', true)
        ->call('addProduct', $product->getKey())
        ->call('addCollection', $collection->getKey())
        ->call('save')
        ->assertHasNoErrors();

    $discount = Discount::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('code', 'VIP25')
        ->firstOrFail();

    expect($discount->value_type)->toBe(DiscountValueType::Fixed)
        ->and($discount->value_amount)->toBe(500)
        ->and($discount->usage_limit)->toBe(100)
        ->and(data_get($discount->rules_json, 'min_purchase_amount'))->toBe(2500)
        ->and(data_get($discount->rules_json, 'one_per_customer'))->toBeTrue()
        ->and(data_get($discount->rules_json, 'applicable_product_ids'))->toBe([$product->getKey()])
        ->and(data_get($discount->rules_json, 'applicable_collection_ids'))->toBe([$collection->getKey()]);
});

test('admin discount form edits discounts and rejects another store', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'EDITME',
    ]);
    $otherDiscount = Discount::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'code' => 'OTHEREDIT',
    ]);

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class, ['discount' => $discount])
        ->set('type', 'automatic')
        ->set('valueType', 'free_shipping')
        ->set('isActive', false)
        ->call('save')
        ->assertHasNoErrors();

    expect($discount->refresh()->type)->toBe(DiscountType::Automatic)
        ->and($discount->code)->toBeNull()
        ->and($discount->value_type)->toBe(DiscountValueType::FreeShipping)
        ->and($discount->status)->toBe(DiscountStatus::Disabled);

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class, ['discount' => $otherDiscount])
        ->assertStatus(404);
});

status: completed


command: sed -n '1,260p' app/Livewire/Admin/Discounts/Index.php

aggregated_output:
<?php

namespace App\Livewire\Admin\Discounts;

use App\Enums\DiscountStatus;
use App\Enums\DiscountValueType;
use App\Models\Discount;
use App\Models\Store;
use App\Support\Money;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

    public string $statusFilter = 'all';

    public function mount(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeId = $store->getKey();
        $this->storeCurrency = $store->default_currency;
    }

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatusFilter(): void
    {
        $this->resetPage();
    }

    public function discounts(): LengthAwarePaginator
    {
        return Discount::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('code', 'like', $search)
                        ->orWhere('type', 'like', $search);
                });
            })
            ->when($this->statusFilter !== 'all', fn (Builder $query) => $this->applyStatusFilter($query))
            ->latest('starts_at')
            ->latest('id')
            ->paginate(20);
    }

    public function valueLabel(Discount $discount): string
    {
        return match ($discount->value_type) {
            DiscountValueType::Percent => $discount->value_amount.'%',
            DiscountValueType::Fixed => Money::format($discount->value_amount, $this->storeCurrency),
            DiscountValueType::FreeShipping => 'Free shipping',
        };
    }

    public function effectiveStatus(Discount $discount): string
    {
        if ($discount->status === DiscountStatus::Disabled) {
            return 'disabled';
        }

        if ($discount->starts_at->isFuture()) {
            return 'scheduled';
        }

        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
            return 'expired';
        }

        return $discount->status->value;
    }

    public function statusColor(string $status): string
    {
        return match ($status) {
            'active' => 'green',
            'expired' => 'red',
            'scheduled' => 'amber',
            default => 'zinc',
        };
    }

    public function render(): mixed
    {
        return view('livewire.admin.discounts.index', [
            'discounts' => $this->discounts(),
        ])->layout('layouts.app', [
            'title' => __('Discounts'),
        ]);
    }

    private function applyStatusFilter(Builder $query): void
    {
        match ($this->statusFilter) {
            'active' => $query
                ->where('status', DiscountStatus::Active)
                ->where('starts_at', '<=', now())
                ->where(function (Builder $query): void {
                    $query->whereNull('ends_at')->orWhere('ends_at', '>=', now());
                }),
            'expired' => $query->where(function (Builder $query): void {
                $query->where('status', DiscountStatus::Expired)->orWhere('ends_at', '<', now());
            }),
            'scheduled' => $query->where('starts_at', '>', now()),
            default => null,
        };
    }
}


command: sed -n '1,360p' app/Livewire/Admin/Discounts/Form.php

aggregated_output:
<?php

namespace App\Livewire\Admin\Discounts;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Collection as ProductCollection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Support\Money;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;

class Form extends Component
{
    public ?Discount $discount = null;

    public string $type = 'code';

    public string $code = '';

    public string $valueType = 'percent';

    public string $valueAmount = '10';

    public string $minimumPurchaseAmount = '';

    /**
     * @var array<int, int>
     */
    public array $specificProductIds = [];

    /**
     * @var array<int, int>
     */
    public array $specificCollectionIds = [];

    public string $usageLimit = '';

    public bool $onePerCustomer = false;

    public string $startsAt = '';

    public string $endsAt = '';

    public bool $isActive = true;

    public string $productSearch = '';

    public string $collectionSearch = '';

    public string $storeCurrency = 'EUR';

    public function mount(?Discount $discount = null): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeCurrency = $store->default_currency;

        if ($discount?->exists) {
            abort_unless((int) $discount->store_id === $store->getKey(), 404);

            $this->discount = $discount;
            $this->fillFromDiscount($discount);

            return;
        }

        $this->startsAt = now()->format('Y-m-d\TH:i');
    }

    public function save(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->validate([
            'type' => ['required', Rule::in(['code', 'automatic'])],
            'code' => [
                Rule::requiredIf($this->type === 'code'),
                'nullable',
                'string',
                'max:255',
                Rule::unique('discounts', 'code')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->discount?->getKey()),
            ],
            'valueType' => ['required', Rule::in(['percent', 'fixed', 'free_shipping'])],
            'valueAmount' => [Rule::requiredIf($this->valueType !== 'free_shipping'), 'nullable', 'numeric', 'min:0.01'],
            'minimumPurchaseAmount' => ['nullable', 'numeric', 'min:0'],
            'usageLimit' => ['nullable', 'integer', 'min:1'],
            'startsAt' => ['required', 'date'],
            'endsAt' => ['nullable', 'date', 'after:startsAt'],
            'onePerCustomer' => ['boolean'],
            'isActive' => ['boolean'],
        ]);

        $discount = $this->discount instanceof Discount
            ? tap($this->discount)->update($this->payload($store))
            : Discount::withoutGlobalScopes()->create($this->payload($store));

        $this->discount = $discount->refresh();
        $this->fillFromDiscount($this->discount);

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

    public function generateCode(): void
    {
        do {
            $code = Str::upper(Str::random(8));
        } while (Discount::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->where('code', $code)
            ->exists());

        $this->code = $code;
    }

    public function addProduct(int $productId): void
    {
        abort_unless($this->productBelongsToStore($productId), 404);

        $this->specificProductIds = collect([...$this->specificProductIds, $productId])
            ->unique()
            ->values()
            ->all();
        $this->productSearch = '';
    }

    public function removeProduct(int $productId): void
    {
        $this->specificProductIds = array_values(array_filter(
            $this->specificProductIds,
            fn (int $selectedProductId): bool => $selectedProductId !== $productId,
        ));
    }

    public function addCollection(int $collectionId): void
    {
        abort_unless($this->collectionBelongsToStore($collectionId), 404);

        $this->specificCollectionIds = collect([...$this->specificCollectionIds, $collectionId])
            ->unique()
            ->values()
            ->all();
        $this->collectionSearch = '';
    }

    public function removeCollection(int $collectionId): void
    {
        $this->specificCollectionIds = array_values(array_filter(
            $this->specificCollectionIds,
            fn (int $selectedCollectionId): bool => $selectedCollectionId !== $collectionId,
        ));
    }

    public function productResults(): SupportCollection
    {
        if (trim($this->productSearch) === '') {
            return collect();
        }

        return Product::withoutGlobalScopes()
    …1228 chars truncated…         ->orderBy('title')
            ->get();
    }

    public function render(): mixed
    {
        return view('livewire.admin.discounts.form', [
            'isEditing' => $this->discount instanceof Discount,
            'productResults' => $this->productResults(),
            'collectionResults' => $this->collectionResults(),
            'selectedProducts' => $this->selectedProducts(),
            'selectedCollections' => $this->selectedCollections(),
        ])->layout('layouts.app', [
            'title' => $this->discount ? __('Edit discount') : __('Create discount'),
        ]);
    }

    private function fillFromDiscount(Discount $discount): void
    {
        $this->type = $discount->type->value;
        $this->code = (string) $discount->code;
        $this->valueType = $discount->value_type->value;
        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
            ? number_format($discount->value_amount / 100, 2, '.', '')
            : (string) $discount->value_amount;
        $this->minimumPurchaseAmount = data_get($discount->rules_json, 'min_purchase_amount')
            ? number_format(((int) data_get($discount->rules_json, 'min_purchase_amount')) / 100, 2, '.', '')
            : '';
        $this->specificProductIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->specificCollectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
        $this->onePerCustomer = (bool) data_get($discount->rules_json, 'one_per_customer', false);
        $this->startsAt = $discount->starts_at->format('Y-m-d\TH:i');
        $this->endsAt = $discount->ends_at?->format('Y-m-d\TH:i') ?? '';
        $this->isActive = $discount->status !== DiscountStatus::Disabled;
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(Store $store): array
    {
        return [
            'store_id' => $store->getKey(),
            'type' => DiscountType::from($this->type),
            'code' => $this->type === 'code' ? Str::upper(trim($this->code)) : null,
            'value_type' => DiscountValueType::from($this->valueType),
            'value_amount' => $this->normalizedValueAmount(),
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt !== '' ? $this->endsAt : null,
            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
            'rules_json' => $this->rulesPayload(),
            'status' => $this->isActive ? DiscountStatus::Active : DiscountStatus::Disabled,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function rulesPayload(): array
    {
        return [
            'min_purchase_amount' => $this->minimumPurchaseAmount !== '' ? Money::fromDecimalString($this->minimumPurchaseAmount) : 0,
            'applicable_product_ids' => $this->validProductIds(),
            'applicable_collection_ids' => $this->validCollectionIds(),
            'one_per_customer' => $this->onePerCustomer,
            'customer_eligibility' => 'all',
        ];
    }

    private function normalizedValueAmount(): int
    {
        return match ($this->valueType) {
            'fixed' => Money::fromDecimalString($this->valueAmount),
            'free_shipping' => 0,
            default => (int) round((float) $this->valueAmount),
        };
    }

    /**
     * @return array<int, int>
     */
    private function validProductIds(): array
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificProductIds)
            ->pluck('id')
            ->map(fn (int $id): int => $id)
            ->all();
    }

    /**
     * @return array<int, int>
     */
    private function validCollectionIds(): array
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificCollectionIds)
            ->pluck('id')
            ->map(fn (int $id): int => $id)
            ->all();
    }

    private function productBelongsToStore(int $productId): bool
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($productId)
            ->exists();
    }

    private function collectionBelongsToStore(int $collectionId): bool
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($collectionId)
            ->exists();
    }

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

        abort_unless($store instanceof Store, 404);

        return $store->getKey();
    }
}


command: sed -n '1,220p' tests/Feature/Admin/CustomerManagementTest.php

aggregated_output:
<?php

use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminCustomerManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

function adminCustomerManagementCustomer(Store $store, string $email, string $name): Customer
{
    return Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => $email,
        'name' => $name,
    ]);
}

function adminCustomerManagementOrder(Store $store, Customer $customer, string $orderNumber, int $totalAmount): Order
{
    return Order::factory()->paid()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'subtotal_amount' => $totalAmount,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $totalAmount,
        'email' => $customer->email,
    ]);
}

test('admin customer routes require authentication and render store scoped customers', function (): void {
    $store = adminCustomerManagementStore();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementOrder($store, $customer, '#8101', 7000);
    CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    $this->get('/admin/customers')->assertRedirect('/admin/login');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers')
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('70.00 EUR')
        ->assertDontSee('Other Customer');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$customer->getKey())
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('#8101')
        ->assertSee('Home');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$otherCustomer->getKey())
        ->assertNotFound();
});

test('admin customer index filters by name and email', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementCustomer($store, 'bravo@example.test', 'Bravo Example');

    Livewire::actingAs($user)
        ->test(AdminCustomersIndex::class)
        ->assertSee('Jane Example')
        ->assertSee('Bravo Example')
        ->set('search', 'jane')
        ->assertSee('Jane Example')
        ->assertDontSee('Bravo Example')
        ->set('search', 'bravo@example.test')
        ->assertSee('Bravo Example')
        ->assertDontSee('Jane Example');
});

test('admin customer detail manages addresses', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    $home = CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('openAddressForm')
        ->set('addressLabel', 'Office')
        ->set('addressJson.address1', 'Business Street 5')
        ->set('addressJson.city', 'Berlin')
        ->set('addressJson.postal_code', '10115')
        ->set('addressJson.country', 'DE')
        ->call('saveAddress')
        ->assertHasNoErrors();

    $office = CustomerAddress::query()
        ->where('customer_id', $customer->getKey())
        ->where('label', 'Office')
        ->firstOrFail();

    expect($office->is_default)->toBeFalse();

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('setDefaultAddress', $office->getKey())
        ->assertHasNoErrors()
        ->call('openAddressForm', $office->getKey())
        ->set('addressLabel', 'HQ')
        ->call('saveAddress')
        ->assertHasNoErrors()
        ->call('deleteAddress', $home->getKey())
        ->assertHasNoErrors();

    expect($office->refresh()->is_default)->toBeTrue()
        ->and($office->label)->toBe('HQ')
        ->and(CustomerAddress::query()->whereKey($home->getKey())->exists())->toBeFalse();
});

test('admin customer detail rejects customers from another store', function (): void {
    $store = adminCustomerManagementStore();
    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

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

    Livewire::actingAs(adminCustomerManagementUser())
        ->test(AdminCustomerShow::class, ['customer' => $otherCustomer])
        ->assertStatus(404);
});


command: sed -n '1,220p' tests/Feature/Admin/OrderManagementTest.php

aggregated_output:
<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminOrderManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

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

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

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin Test Product',
        'sku_snapshot' => 'ADMIN-TEST-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

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

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

test('admin order routes require authentication and render scoped orders', function () {
    $store = adminOrderManagementStore();
    [$order] = adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);

    $otherStore = Store::factory()->create();
    adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->get('/admin/orders')->assertRedirect('/admin/login');

    $user = adminOrderManagementUser();

    $this->actingAs($user)
        ->get('/admin/orders')
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertDontSee('#9001');

    $this->actingAs($user)
        ->get('/admin/orders/'.$order->getKey())
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertSee('Admin Test Product');
});

test('admin order index filters by search and status dimensions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);
    adminOrderManagementOrder($store, [
        'order_number' => '#7002',
        'email' => 'bravo@example.test',
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
        'payment_method' => PaymentMethod::BankTransfer,
    ]);

    Livewire::actingAs($user)
        ->test(AdminOrdersIndex::class)
        ->assertSee('#7001')
        ->assertSee('#7002')
        ->set('search', 'alpha')
        ->assertSee('#7001')
        ->assertDontSee('#7002')
        ->set('search', '')
        ->set('financialStatusFilter', 'pending')
        ->assertSee('#7002')
        ->assertDontSee('#7001');
});

test('admin order detail confirms bank transfer payments', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line, $variant] = adminOrderManagementOrder($store, [
        'payment_method' => PaymentMethod::BankTransfer,
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
    ]);

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

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->call('confirmBankTransferPayment')
        ->assertHasNoErrors();

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

    expect($order->refresh()->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->payments()->first()?->status)->toBe(PaymentStatus::Captured)
        ->and($inventory->quantity_on_hand)->toBe(8)
        ->and($inventory->quantity_reserved)->toBe(0);
});

test('admin order detail processes refunds and fulfillment transitions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line] = adminOrderManagementOrder($store, quantity: 2, unitPrice: 2500);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->set('refundAmount', '10.00')
        ->set('refundReason', 'Customer return')
        ->call('processRefund')
        ->assertHasNoErrors();

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded)
        ->and($order->refunds()->first()?->amount)->toBe(1000);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->set("fulfillmentLineQuantities.{$line->getKey()}", 1)
        ->set('trackingCompany', 'DHL')
        ->set('trackingNumber', 'DHL123')
        ->call('createFulfillment')
        ->assertHasNoErrors();

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

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->lines()->first()?->quantity)->toBe(1);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->call('markFulfillmentShipped', $fulfillment->getKey())
        ->call('markFulfillmentDelivered', $fulfillment->getKey())
        ->assertHasNoErrors();

    expect($fulfillment->refresh()->status)->toBe(FulfillmentShipmentStatus::Delivered)
        ->and($fulfillment->delivered_at)->not->toBeNull();
});

test('admin order detail rejects orders from another store', function () {
    $store = adminOrderManagementStore();
    $otherStore = Store::factory()->create();
    $user = adminOrderManagementUser();


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

type: unknown

cmd: git status --short

aggregated_output:
 M resources/views/layouts/app/sidebar.blade.php
 M routes/web.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T01-43-16-081Z.log
?? app/Livewire/Admin/Discounts/
?? resources/views/livewire/admin/discounts/
?? tests/Feature/Admin/DiscountManagementTest.php


output:
Chunk ID: 98a479
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 77
Output:
 M resources/views/layouts/app/sidebar.blade.php
 M routes/web.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T01-43-16-081Z.log
?? app/Livewire/Admin/Discounts/
?? resources/views/livewire/admin/discounts/
?? tests/Feature/Admin/DiscountManagementTest.php


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

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
use App\Models\Collection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminDiscountManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

test('admin discount routes require authentication and render store scoped discounts', function (): void {
    $store = adminDiscountManagementStore();
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SAVE20',
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 20,
        'usage_count' => 3,
        'usage_limit' => 100,
    ]);
    Discount::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'code' => 'OTHER20',
    ]);

    $this->get('/admin/discounts')->assertRedirect('/admin/login');

    $this->actingAs(adminDiscountManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/discounts')
        ->assertSuccessful()
        ->assertSee('SAVE20')
        ->assertSee('20%')
        ->assertSee('3 / 100')
        ->assertDontSee('OTHER20');

    $this->actingAs(adminDiscountManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/discounts/create')
        ->assertSuccessful()
        ->assertSee('Create discount');
});

test('admin discount index filters by code and effective status', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'ACTIVE10',
    ]);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SOON10',
        'starts_at' => now()->addWeek(),
    ]);

    Livewire::actingAs($user)
        ->test(AdminDiscountsIndex::class)
        ->assertSee('ACTIVE10')
        ->assertSee('SOON10')
        ->set('search', 'active')
        ->assertSee('ACTIVE10')
        ->assertDontSee('SOON10')
        ->set('search', '')
        ->set('statusFilter', 'scheduled')
        ->assertSee('SOON10')
        ->assertDontSee('ACTIVE10');
});

test('admin discount form creates discounts with eligibility rules', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    $product = Product::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
    $collection = Collection::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class)
        ->set('type', 'code')
        ->set('code', 'VIP25')
        ->set('valueType', 'fixed')
        ->set('valueAmount', '5.00')
        ->set('minimumPurchaseAmount', '25.00')
        ->set('usageLimit', '100')
        ->set('onePerCustomer', true)
        ->call('addProduct', $product->getKey())
        ->call('addCollection', $collection->getKey())
        ->call('save')
        ->assertHasNoErrors();

    $discount = Discount::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('code', 'VIP25')
        ->firstOrFail();

    expect($discount->value_type)->toBe(DiscountValueType::Fixed)
        ->and($discount->value_amount)->toBe(500)
        ->and($discount->usage_limit)->toBe(100)
        ->and(data_get($discount->rules_json, 'min_purchase_amount'))->toBe(2500)
        ->and(data_get($discount->rules_json, 'one_per_customer'))->toBeTrue()
        ->and(data_get($discount->rules_json, 'applicable_product_ids'))->toBe([$product->getKey()])
        ->and(data_get($discount->rules_json, 'applicable_collection_ids'))->toBe([$collection->getKey()]);
});

test('admin discount form edits discounts and rejects another store', function (): void {
    $store = adminDiscountManagementStore();
    $user = adminDiscountManagementUser();
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'EDITME',
    ]);
    $otherDiscount = Discount::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
        'code' => 'OTHEREDIT',
    ]);

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class, ['discount' => $discount])
        ->set('type', 'automatic')
        ->set('valueType', 'free_shipping')
        ->set('isActive', false)
        ->call('save')
        ->assertHasNoErrors();

    expect($discount->refresh()->type)->toBe(DiscountType::Automatic)
        ->and($discount->code)->toBeNull()
        ->and($discount->value_type)->toBe(DiscountValueType::FreeShipping)
        ->and($discount->status)->toBe(DiscountStatus::Disabled);

    Livewire::actingAs($user)
        ->test(AdminDiscountForm::class, ['discount' => $otherDiscount])
        ->assertStatus(404);
});


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

namespace App\Livewire\Admin\Discounts;

use App\Enums\DiscountStatus;
use App\Enums\DiscountValueType;
use App\Models\Discount;
use App\Models\Store;
use App\Support\Money;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

    public string $statusFilter = 'all';

    public function mount(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeId = $store->getKey();
        $this->storeCurrency = $store->default_currency;
    }

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatusFilter(): void
    {
        $this->resetPage();
    }

    public function discounts(): LengthAwarePaginator
    {
        return Discount::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('code', 'like', $search)
                        ->orWhere('type', 'like', $search);
                });
            })
            ->when($this->statusFilter !== 'all', fn (Builder $query) => $this->applyStatusFilter($query))
            ->latest('starts_at')
            ->latest('id')
            ->paginate(20);
    }

    public function valueLabel(Discount $discount): string
    {
        return match ($discount->value_type) {
            DiscountValueType::Percent => $discount->value_amount.'%',
            DiscountValueType::Fixed => Money::format($discount->value_amount, $this->storeCurrency),
            DiscountValueType::FreeShipping => 'Free shipping',
        };
    }

    public function effectiveStatus(Discount $discount): string
    {
        if ($discount->status === DiscountStatus::Disabled) {
            return 'disabled';
        }

        if ($discount->starts_at->isFuture()) {
            return 'scheduled';
        }

        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
            return 'expired';
        }

        return $discount->status->value;
    }

    public function statusColor(string $status): string
    {
        return match ($status) {
            'active' => 'green',
            'expired' => 'red',
            'scheduled' => 'amber',
            default => 'zinc',
        };
    }

    public function render(): mixed
    {
        return view('livewire.admin.discounts.index', [
            'discounts' => $this->discounts(),
        ])->layout('layouts.app', [
            'title' => __('Discounts'),
        ]);
    }

    private function applyStatusFilter(Builder $query): void
    {
        match ($this->statusFilter) {
            'active' => $query
                ->where('status', DiscountStatus::Active)
                ->where('starts_at', '<=', now())
                ->where(function (Builder $query): void {
                    $query->whereNull('ends_at')->orWhere('ends_at', '>=', now());
                }),
            'expired' => $query->where(function (Builder $query): void {
                $query->where('status', DiscountStatus::Expired)->orWhere('ends_at', '<', now());
            }),
            'scheduled' => $query->where('starts_at', '>', now()),
            default => null,
        };
    }
}


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

namespace App\Livewire\Admin\Discounts;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Collection as ProductCollection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Support\Money;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;

class Form extends Component
{
    public ?Discount $discount = null;

    public string $type = 'code';

    public string $code = '';

    public string $valueType = 'percent';

    public string $valueAmount = '10';

    public string $minimumPurchaseAmount = '';

    /**
     * @var array<int, int>
     */
    public array $specificProductIds = [];

    /**
     * @var array<int, int>
     */
    public array $specificCollectionIds = [];

    public string $usageLimit = '';

    public bool $onePerCustomer = false;

    public string $startsAt = '';

    public string $endsAt = '';

    public bool $isActive = true;

    public string $productSearch = '';

    public string $collectionSearch = '';

    public string $storeCurrency = 'EUR';

    public function mount(?Discount $discount = null): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeCurrency = $store->default_currency;

        if ($discount?->exists) {
            abort_unless((int) $discount->store_id === $store->getKey(), 404);

            $this->discount = $discount;
            $this->fillFromDiscount($discount);

            return;
        }

        $this->startsAt = now()->format('Y-m-d\TH:i');
    }

    public function save(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->validate([
            'type' => ['required', Rule::in(['code', 'automatic'])],
            'code' => [
                Rule::requiredIf($this->type === 'code'),
                'nullable',
                'string',
                'max:255',
                Rule::unique('discounts', 'code')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->discount?->getKey()),
            ],
            'valueType' => ['required', Rule::in(['percent', 'fixed', 'free_shipping'])],
            'valueAmount' => [Rule::requiredIf($this->valueType !== 'free_shipping'), 'nullable', 'numeric', 'min:0.01'],
            'minimumPurchaseAmount' => ['nullable', 'numeric', 'min:0'],
            'usageLimit' => ['nullable', 'integer', 'min:1'],
            'startsAt' => ['required', 'date'],
            'endsAt' => ['nullable', 'date', 'after:startsAt'],
            'onePerCustomer' => ['boolean'],
            'isActive' => ['boolean'],
        ]);

        $discount = $this->discount instanceof Discount
            ? tap($this->discount)->update($this->payload($store))
            : Discount::withoutGlobalScopes()->create($this->payload($store));

        $this->discount = $discount->refresh();
        $this->fillFromDiscount($this->discount);

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

    public function generateCode(): void
    {
        do {
            $code = Str::upper(Str::random(8));
        } while (Discount::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->where('code', $code)
            ->exists());

        $this->code = $code;
    }

    public function addProduct(int $productId): void
    {
        abort_unless($this->productBelongsToStore($productId), 404);

        $this->specificProductIds = collect([...$this->specificProductIds, $productId])
            ->unique()
            ->values()
            ->all();
        $this->productSearch = '';
    }

    public function removeProduct(int $productId): void
    {
        $this->specificProductIds = array_values(array_filter(
            $this->specificProductIds,
            fn (int $selectedProductId): bool => $selectedProductId !== $productId,
        ));
    }

    public function addCollection(int $collectionId): void
    {
        abort_unless($this->collectionBelongsToStore($collectionId), 404);

        $this->specificCollectionIds = collect([...$this->specificCollectionIds, $collectionId])
            ->unique()
            ->values()
            ->all();
        $this->collectionSearch = '';
    }

    public function removeCollection(int $collectionId): void
    {
        $this->specificCollectionIds = array_values(array_filter(
            $this->specificCollectionIds,
            fn (int $selectedCollectionId): bool => $selectedCollectionId !== $collectionId,
        ));
    }

    public function productResults(): SupportCollection
    {
        if (trim($this->productSearch) === '') {
            return collect();
        }

        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereNotIn('id', $this->specificProductIds)
            ->where('title', 'like', '%'.$this->productSearch.'%')
            ->orderBy('title')
            ->limit(5)
            ->get();
    }

    public function collectionResults(): SupportCollection
    {
        if (trim($this->collectionSearch) === '') {
            return collect();
        }

        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereNotIn('id', $this->specificCollectionIds)
            ->where('title', 'like', '%'.$this->collectionSearch.'%')
            ->orderBy('title')
            ->limit(5)
            ->get();
    }

    public function selectedProducts(): SupportCollection
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificProductIds)
            ->orderBy('title')
            ->get();
    }

    public function selectedCollections(): SupportCollection
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificCollectionIds)
            ->orderBy('title')
            ->get();
    }

    public function render(): mixed
    {
        return view('livewire.admin.discounts.form', [
            'isEditing' => $this->discount instanceof Discount,
            'productResults' => $this->productResults(),
            'collectionResults' => $this->collectionResults(),
            'selectedProducts' => $this->selectedProducts(),
            'selectedCollections' => $this->selectedCollections(),
        ])->layout('layouts.app', [
            'title' => $this->discount ? __('Edit discount') : __('Create discount'),
        ]);
    }

    private function fillFromDiscount(Discount $discount): void
    {
        $this->type = $discount->type->value;
        $this->code = (string) $discount->code;
        $this->valueType = $discount->value_type->value;
        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
            ? number_format($discount->value_amount / 100, 2, '.', '')
            : (string) $discount->value_amount;
        $this->minimumPurchaseAmount = data_get($discount->rules_json, 'min_purchase_amount')
            ? number_format(((int) data_get($discount->rules_json, 'min_purchase_amount')) / 100, 2, '.', '')
            : '';
        $this->specificProductIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->specificCollectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
        $this->onePerCustomer = (bool) data_get($discount->rules_json, 'one_per_customer', false);
        $this->startsAt = $discount->starts_at->format('Y-m-d\TH:i');
        $this->endsAt = $discount->ends_at?->format('Y-m-d\TH:i') ?? '';
        $this->isActive = $discount->status !== DiscountStatus::Disabled;
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(Store $store): array
    {
        return [
            'store_id' => $store->getKey(),
            'type' => DiscountType::from($this->type),
            'code' => $this->type === 'code' ? Str::upper(trim($this->code)) : null,
            'value_type' => DiscountValueType::from($this->valueType),
            'value_amount' => $this->normalizedValueAmount(),
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt !== '' ? $this->endsAt : null,
            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
            'rules_json' => $this->rulesPayload(),
            'status' => $this->isActive ? DiscountStatus::Active : DiscountStatus::Disabled,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function rulesPayload(): array
    {
        return [
            'min_purchase_amount' => $this->minimumPurchaseAmount !== '' ? Money::fromDecimalString($this->minimumPurchaseAmount) : 0,
            'applicable_product_ids' => $this->validProductIds(),
            'applicable_collection_ids' => $this->validCollectionIds(),
            'one_per_customer' => $this->onePerCustomer,
            'customer_eligibility' => 'all',
        ];
    }

    private function normalizedValueAmount(): int
    {
        return match ($this->valueType) {
            'fixed' => Money::fromDecimalString($this->valueAmount),
            'free_shipping' => 0,
            default => (int) round((float) $this->valueAmount),
        };
    }

    /**
     * @return array<int, int>
     */
    private function validProductIds(): array
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificProductIds)
            ->pluck('id')
            ->map(fn (int $id): int => $id)
            ->all();
    }

    /**
     * @return array<int, int>
     */
    private function validCollectionIds(): array
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificCollectionIds)
            ->pluck('id')
            ->map(fn (int $id): int => $id)
            ->all();
    }

    private function productBelongsToStore(int $productId): bool
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($productId)
            ->exists();
    }

    private function collectionBelongsToStore(int $collectionId): bool
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($collectionId)
            ->exists();
    }

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

        abort_unless($store instanceof Store, 404);

        return $store->getKey();
    }
}


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

use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminCustomerManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

function adminCustomerManagementCustomer(Store $store, string $email, string $name): Customer
{
    return Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => $email,
        'name' => $name,
    ]);
}

function adminCustomerManagementOrder(Store $store, Customer $customer, string $orderNumber, int $totalAmount): Order
{
    return Order::factory()->paid()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'subtotal_amount' => $totalAmount,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $totalAmount,
        'email' => $customer->email,
    ]);
}

test('admin customer routes require authentication and render store scoped customers', function (): void {
    $store = adminCustomerManagementStore();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementOrder($store, $customer, '#8101', 7000);
    CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    $this->get('/admin/customers')->assertRedirect('/admin/login');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers')
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('70.00 EUR')
        ->assertDontSee('Other Customer');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$customer->getKey())
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('#8101')
        ->assertSee('Home');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$otherCustomer->getKey())
        ->assertNotFound();
});

test('admin customer index filters by name and email', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementCustomer($store, 'bravo@example.test', 'Bravo Example');

    Livewire::actingAs($user)
        ->test(AdminCustomersIndex::class)
        ->assertSee('Jane Example')
        ->assertSee('Bravo Example')
        ->set('search', 'jane')
        ->assertSee('Jane Example')
        ->assertDontSee('Bravo Example')
        ->set('search', 'bravo@example.test')
        ->assertSee('Bravo Example')
        ->assertDontSee('Jane Example');
});

test('admin customer detail manages addresses', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    $home = CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('openAddressForm')
        ->set('addressLabel', 'Office')
        ->set('addressJson.address1', 'Business Street 5')
        ->set('addressJson.city', 'Berlin')
        ->set('addressJson.postal_code', '10115')
        ->set('addressJson.country', 'DE')
        ->call('saveAddress')
        ->assertHasNoErrors();

    $office = CustomerAddress::query()
        ->where('customer_id', $customer->getKey())
        ->where('label', 'Office')
        ->firstOrFail();

    expect($office->is_default)->toBeFalse();

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('setDefaultAddress', $office->getKey())
        ->assertHasNoErrors()
        ->call('openAddressForm', $office->getKey())
        ->set('addressLabel', 'HQ')
        ->call('saveAddress')
        ->assertHasNoErrors()
        ->call('deleteAddress', $home->getKey())
        ->assertHasNoErrors();

    expect($office->refresh()->is_default)->toBeTrue()
        ->and($office->label)->toBe('HQ')
        ->and(CustomerAddress::query()->whereKey($home->getKey())->exists())->toBeFalse();
});

test('admin customer detail rejects customers from another store', function (): void {
    $store = adminCustomerManagementStore();
    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

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

    Livewire::actingAs(adminCustomerManagementUser())
        ->test(AdminCustomerShow::class, ['customer' => $otherCustomer])
        ->assertStatus(404);
});


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

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminOrderManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

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

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

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

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin Test Product',
        'sku_snapshot' => 'ADMIN-TEST-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

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

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

test('admin order routes require authentication and render scoped orders', function () {
    $store = adminOrderManagementStore();
    [$order] = adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);

    $otherStore = Store::factory()->create();
    adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->get('/admin/orders')->assertRedirect('/admin/login');

    $user = adminOrderManagementUser();

    $this->actingAs($user)
        ->get('/admin/orders')
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertDontSee('#9001');

    $this->actingAs($user)
        ->get('/admin/orders/'.$order->getKey())
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertSee('Admin Test Product');
});

test('admin order index filters by search and status dimensions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);
    adminOrderManagementOrder($store, [
        'order_number' => '#7002',
        'email' => 'bravo@example.test',
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
        'payment_method' => PaymentMethod::BankTransfer,
    ]);

    Livewire::actingAs($user)
        ->test(AdminOrdersIndex::class)
        ->assertSee('#7001')
        ->assertSee('#7002')
        ->set('search', 'alpha')
        ->assertSee('#7001')
        ->assertDontSee('#7002')
        ->set('search', '')
        ->set('financialStatusFilter', 'pending')
        ->assertSee('#7002')
        ->assertDontSee('#7001');
});

test('admin order detail confirms bank transfer payments', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line, $variant] = adminOrderManagementOrder($store, [
        'payment_method' => PaymentMethod::BankTransfer,
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
    ]);

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

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->call('confirmBankTransferPayment')
        ->assertHasNoErrors();

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

    expect($order->refresh()->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->payments()->first()?->status)->toBe(PaymentStatus::Captured)
        ->and($inventory->quantity_on_hand)->toBe(8)
        ->and($inventory->quantity_reserved)->toBe(0);
});

test('admin order detail processes refunds and fulfillment transitions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line] = adminOrderManagementOrder($store, quantity: 2, unitPrice: 2500);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->set('refundAmount', '10.00')
        ->set('refundReason', 'Customer return')
        ->call('processRefund')
        ->assertHasNoErrors();

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded)
        ->and($order->refunds()->first()?->amount)->toBe(1000);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->set("fulfillmentLineQuantities.{$line->getKey()}", 1)
        ->set('trackingCompany', 'DHL')
        ->set('trackingNumber', 'DHL123')
        ->call('createFulfillment')
        ->assertHasNoErrors();

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

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->lines()->first()?->quantity)->toBe(1);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->call('markFulfillmentShipped', $fulfillment->getKey())
        ->call('markFulfillmentDelivered', $fulfillment->getKey())
        ->assertHasNoErrors();

    expect($fulfillment->refresh()->status)->toBe(FulfillmentShipmentStatus::Delivered)
        ->and($fulfillment->delivered_at)->not->toBeNull();
});

test('admin order detail rejects orders from another store', function () {
    $store = adminOrderManagementStore();
    $otherStore = Store::factory()->create();
    $user = adminOrderManagementUser();


message: The discount tests are already populated and match the surrounding admin test style. I found one small cleanup in the index component before running them: an unused import and enum values in query filters that should be bound as scalar values.


text: The discount tests are already populated and match the surrounding admin test style. I found one small cleanup in the index component before running them: an unused import and enum values in query filters that should be bound as scalar values.


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


cmd: sed -n '1,260p' database/factories/DiscountFactory.php


cmd: sed -n '1,260p' resources/views/livewire/admin/discounts/index.blade.php


cmd: sed -n '1,360p' resources/views/livewire/admin/discounts/form.blade.php


command:
/bin/zsh
-lc
sed -n '1,260p' app/Models/Discount.php

Discount.php

cmd: sed -n '1,260p' app/Models/Discount.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'type',
        'code',
        'value_type',
        'value_amount',
        'starts_at',
        'ends_at',
        'usage_limit',
        'usage_count',
        'rules_json',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'code',
        'value_amount' => 0,
        'usage_count' => 0,
        'rules_json' => '{}',
        'status' => 'active',
    ];

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => DiscountType::class,
            'value_type' => DiscountValueType::class,
            'value_amount' => 'integer',
            'starts_at' => 'datetime',
            'ends_at' => 'datetime',
            'usage_limit' => 'integer',
            'usage_count' => 'integer',
            'rules_json' => 'array',
            'status' => DiscountStatus::class,
        ];
    }
}


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

aggregated_output:
<?php

namespace App\Services;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Exceptions\InvalidDiscountException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Discount;
use App\Models\ProductVariant;
use App\Models\Store;
use App\ValueObjects\DiscountResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DiscountService
{
    public function validate(string $code, Store $store, Cart $cart): Discount
    {
        $normalizedCode = mb_strtolower(trim($code));

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

        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()) {
            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
        }

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

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

        if ($this->qualifyingLines($discount, $lines)->isEmpty()) {
            throw InvalidDiscountException::because('discount_not_applicable', 'Discount does not apply to these cart lines.');
        }

        return $discount;
    }

    /**
     * @param  array<int, CartLine>  $lines
     */
    public function calculate(Discount $discount, int $subtotal, array $lines): DiscountResult
    {
        if ($discount->value_type === DiscountValueType::FreeShipping) {
            return new DiscountResult(0, [], true);
        }

        $qualifyingLines = $this->qualifyingLines($discount, collect($lines));
        $qualifyingSubtotal = $qualifyingLines->sum('line_subtotal_amount');

        if ($qualifyingSubtotal <= 0) {
            return new DiscountResult(0, []);
        }

        $discountAmount = match ($discount->value_type) {
            DiscountValueType::Percent => (int) round($qualifyingSubtotal * $discount->value_amount / 100),
            DiscountValueType::Fixed => min($discount->value_amount, $qualifyingSubtotal),
            DiscountValueType::FreeShipping => 0,
        };

        $remaining = $discountAmount;
        $allocations = [];
        $lastIndex = $qualifyingLines->keys()->last();

        foreach ($qualifyingLines as $index => $line) {
            if ($index === $lastIndex) {
                $allocations[$line->getKey()] = $remaining;

                continue;
            }

            $allocation = (int) round($discountAmount * $line->line_subtotal_amount / $qualifyingSubtotal);
            $allocations[$line->getKey()] = $allocation;
            $remaining -= $allocation;
        }

        return new DiscountResult($discountAmount, $allocations);
    }

    public function applyToCart(Cart $cart, Discount $discount): DiscountResult
    {
        return DB::transaction(function () use ($cart, $discount): DiscountResult {
            $lines = $this->cartLines($cart);
            $result = $this->calculate($discount, $lines->sum('line_subtotal_amount'), $lines->all());

            $lines->each(function (CartLine $line) use ($result): void {
                $discountAmount = $result->allocations[$line->getKey()] ?? 0;

                $line->forceFill([
                    'line_discount_amount' => $discountAmount,
                    'line_total_amount' => $line->line_subtotal_amount - $discountAmount,
                ])->save();
            });

            return $result;
        });
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  Collection<int, CartLine>  $lines
     * @return Collection<int, CartLine>
     */
    private function qualifyingLines(Discount $discount, Collection $lines): Collection
    {
        $productIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();
        $collectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();

        if ($productIds->isEmpty() && $collectionIds->isEmpty()) {
            return $lines;
        }

        return $lines->filter(function (CartLine $line) use ($productIds, $collectionIds): bool {
            $variant = ProductVariant::withoutGlobalScopes()
                ->with(['product' => fn ($query) => $query->withoutGlobalScopes()->with('collections')])
                ->find($line->variant_id);

            if (! $variant instanceof ProductVariant || $variant->product === null) {
                return false;
            }

            if ($productIds->contains((int) $variant->product_id)) {
                return true;
            }

            return $variant->product->collections
                ->pluck('id')
                ->map(fn (int $id): int => $id)
                ->intersect($collectionIds)
                ->isNotEmpty();
        });
    }
}


command: sed -n '1,260p' database/factories/DiscountFactory.php

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


command: sed -n '1,260p' resources/views/livewire/admin/discounts/index.blade.php

aggregated_output:
            <flux:heading size="xl">Discounts</flux:heading>
            <flux:text class="mt-1">Manage discount codes, automatic promotions, and eligibility rules.</flux:text>

        <flux:button :href="route('admin.discounts.create')" wire:navigate variant="primary" icon="plus">
            Create discount
        </flux:button>

        <flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="Search discounts..." aria-label="Search discounts" />

        <flux:select wire:model.live="statusFilter" aria-label="Status filter">
            <flux:select.option value="all">All statuses</flux:select.option>
            <flux:select.option value="active">Active</flux:select.option>
            <flux:select.option value="scheduled">Scheduled</flux:select.option>
            <flux:select.option value="expired">Expired</flux:select.option>
        </flux:select>

                        Code
                        Type
                        Value
                        Usage
                        Status
                        Dates

                    @forelse ($discounts as $discount)
                        @php
                            $status = $this->effectiveStatus($discount);
                            $statusColor = $this->statusColor($status);
                            $typeColor = $discount->type->value === 'automatic' ? 'sky' : 'zinc';
                        @endphp

getKey() }}">

                                    {{ $discount->code ?: 'Automatic' }}

                                <flux:badge :color="$typeColor">{{ Str::headline($discount->type->value) }}</flux:badge>

                            {{ $this->valueLabel($discount) }}
                            {{ $discount->usage_count }} / {{ $discount->usage_limit ?? 'unlimited' }}

                                <flux:badge :color="$statusColor">{{ Str::headline($status) }}</flux:badge>

                                {{ $discount->starts_at?->format('M j, Y') }}
                                -
                                {{ $discount->ends_at?->format('M j, Y') ?? 'No end' }}

                    @empty

                                        <flux:icon name="tag" class="size-6" />

                                    <flux:heading size="lg">No discounts found</flux:heading>
                                    <flux:text>Discounts will appear here after creation.</flux:text>
                                    <flux:button :href="route('admin.discounts.create')" wire:navigate variant="primary">Create discount</flux:button>

                    @endforelse

    {{ $discounts->links() }}


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

namespace App\Models;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'type',
        'code',
        'value_type',
        'value_amount',
        'starts_at',
        'ends_at',
        'usage_limit',
        'usage_count',
        'rules_json',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'code',
        'value_amount' => 0,
        'usage_count' => 0,
        'rules_json' => '{}',
        'status' => 'active',
    ];

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => DiscountType::class,
            'value_type' => DiscountValueType::class,
            'value_amount' => 'integer',
            'starts_at' => 'datetime',
            'ends_at' => 'datetime',
            'usage_limit' => 'integer',
            'usage_count' => 'integer',
            'rules_json' => 'array',
            'status' => DiscountStatus::class,
        ];
    }
}


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

namespace App\Services;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Exceptions\InvalidDiscountException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Discount;
use App\Models\ProductVariant;
use App\Models\Store;
use App\ValueObjects\DiscountResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DiscountService
{
    public function validate(string $code, Store $store, Cart $cart): Discount
    {
        $normalizedCode = mb_strtolower(trim($code));

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

        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()) {
            throw InvalidDiscountException::because('discount_expired', 'Discount has expired.');
        }

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

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

        if ($this->qualifyingLines($discount, $lines)->isEmpty()) {
            throw InvalidDiscountException::because('discount_not_applicable', 'Discount does not apply to these cart lines.');
        }

        return $discount;
    }

    /**
     * @param  array<int, CartLine>  $lines
     */
    public function calculate(Discount $discount, int $subtotal, array $lines): DiscountResult
    {
        if ($discount->value_type === DiscountValueType::FreeShipping) {
            return new DiscountResult(0, [], true);
        }

        $qualifyingLines = $this->qualifyingLines($discount, collect($lines));
        $qualifyingSubtotal = $qualifyingLines->sum('line_subtotal_amount');

        if ($qualifyingSubtotal <= 0) {
            return new DiscountResult(0, []);
        }

        $discountAmount = match ($discount->value_type) {
            DiscountValueType::Percent => (int) round($qualifyingSubtotal * $discount->value_amount / 100),
            DiscountValueType::Fixed => min($discount->value_amount, $qualifyingSubtotal),
            DiscountValueType::FreeShipping => 0,
        };

        $remaining = $discountAmount;
        $allocations = [];
        $lastIndex = $qualifyingLines->keys()->last();

        foreach ($qualifyingLines as $index => $line) {
            if ($index === $lastIndex) {
                $allocations[$line->getKey()] = $remaining;

                continue;
            }

            $allocation = (int) round($discountAmount * $line->line_subtotal_amount / $qualifyingSubtotal);
            $allocations[$line->getKey()] = $allocation;
            $remaining -= $allocation;
        }

        return new DiscountResult($discountAmount, $allocations);
    }

    public function applyToCart(Cart $cart, Discount $discount): DiscountResult
    {
        return DB::transaction(function () use ($cart, $discount): DiscountResult {
            $lines = $this->cartLines($cart);
            $result = $this->calculate($discount, $lines->sum('line_subtotal_amount'), $lines->all());

            $lines->each(function (CartLine $line) use ($result): void {
                $discountAmount = $result->allocations[$line->getKey()] ?? 0;

                $line->forceFill([
                    'line_discount_amount' => $discountAmount,
                    'line_total_amount' => $line->line_subtotal_amount - $discountAmount,
                ])->save();
            });

            return $result;
        });
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Cart $cart): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->where('cart_id', $cart->getKey())
            ->orderBy('id')
            ->get();
    }

    /**
     * @param  Collection<int, CartLine>  $lines
     * @return Collection<int, CartLine>
     */
    private function qualifyingLines(Discount $discount, Collection $lines): Collection
    {
        $productIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();
        $collectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->filter()
            ->map(fn (mixed $id): int => (int) $id)
            ->values();

        if ($productIds->isEmpty() && $collectionIds->isEmpty()) {
            return $lines;
        }

        return $lines->filter(function (CartLine $line) use ($productIds, $collectionIds): bool {
            $variant = ProductVariant::withoutGlobalScopes()
                ->with(['product' => fn ($query) => $query->withoutGlobalScopes()->with('collections')])
                ->find($line->variant_id);

            if (! $variant instanceof ProductVariant || $variant->product === null) {
                return false;
            }

            if ($productIds->contains((int) $variant->product_id)) {
                return true;
            }

            return $variant->product->collections
                ->pluck('id')
                ->map(fn (int $id): int => $id)
                ->intersect($collectionIds)
                ->isNotEmpty();
        });
    }
}


output:
Chunk ID: fb53d3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 369
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,
        ]);
    }
}