command: sed -n '60,115p' app/Livewire/Admin/Collections/Form.php && sed -n '90,180p' app/Livewire/Admin/Products/Form.php

Form.php

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

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('collections', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->collection?->getKey()),
            ],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
        ]);

        $attributes = [
            'store_id' => $store->getKey(),
            'title' => $this->title,
            'handle' => Str::slug($this->handle),
            'description_html' => $this->descriptionHtml ?: null,
            'type' => 'manual',
            'status' => $this->status,
        ];

        $collection = $this->collection instanceof Collection
            ? tap($this->collection)->update($attributes)
            : Collection::query()->create($attributes);

        $collection->products()->sync(collect($this->assignedProductIds)
            ->values()
            ->mapWithKeys(fn (int $productId, int $position): array => [$productId => ['position' => $position]])
            ->all());

        $this->collection = $collection->refresh()->load('products');
        $this->fillFromCollection($this->collection);

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

    public function searchResults(): SupportCollection
    {
        if (trim($this->productSearch) === '') {
            'values' => '',
        ];
    }

    public function removeOption(int $index): void
    {
        unset($this->options[$index]);
        $this->options = array_values($this->options);
    }

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
            'vendor' => ['nullable', 'string', 'max:255'],
            'productType' => ['nullable', 'string', 'max:255'],
            'tags' => ['nullable', 'string'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('products', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->product?->getKey()),
            ],
            'variants' => ['required', 'array', 'min:1'],
            'variants.*.sku' => ['nullable', 'string', 'max:255'],
            'variants.*.price' => ['required', 'numeric', 'min:0'],
            'variants.*.compareAtPrice' => ['nullable', 'numeric', 'min:0'],
            'variants.*.quantity' => ['required', 'integer', 'min:0'],
            'variants.*.requiresShipping' => ['boolean'],
        ]);

        if (! $this->validateActiveVariantPricing() || ! $this->validateVariantSkus($store)) {
            return;
        }

        $product = DB::transaction(function () use ($store): Product {
            $productService = app(ProductService::class);

            if ($this->product === null) {
                $product = $productService->create($store, [
                    ...$this->productPayload(),
                    'options' => $this->optionPayload(),
                    'variants' => $this->variantPayload($store),
                ]);
            } else {
                $payload = $this->productPayload();
                $requestedStatus = ProductStatus::from((string) $payload['status']);
                unset($payload['status']);

                $product = $productService->update($this->product, $payload);
                $this->syncExistingVariants($product, $store);

                if ($requestedStatus !== $product->refresh()->status) {
                    $productService->transitionStatus($product, $requestedStatus);
                }
            }

            $product->collections()->sync($this->collectionIds);

            return $product->refresh();
        });

        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections']);
        $this->fillFromProduct($this->product);

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

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,


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

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

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('collections', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->collection?->getKey()),
            ],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
        ]);

        $attributes = [
            'store_id' => $store->getKey(),
            'title' => $this->title,
            'handle' => Str::slug($this->handle),
            'description_html' => $this->descriptionHtml ?: null,
            'type' => 'manual',
            'status' => $this->status,
        ];

        $collection = $this->collection instanceof Collection
            ? tap($this->collection)->update($attributes)
            : Collection::query()->create($attributes);

        $collection->products()->sync(collect($this->assignedProductIds)
            ->values()
            ->mapWithKeys(fn (int $productId, int $position): array => [$productId => ['position' => $position]])
            ->all());

        $this->collection = $collection->refresh()->load('products');
        $this->fillFromCollection($this->collection);

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

    public function searchResults(): SupportCollection
    {
        if (trim($this->productSearch) === '') {
            'values' => '',
        ];
    }

    public function removeOption(int $index): void
    {
        unset($this->options[$index]);
        $this->options = array_values($this->options);
    }

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
            'vendor' => ['nullable', 'string', 'max:255'],
            'productType' => ['nullable', 'string', 'max:255'],
            'tags' => ['nullable', 'string'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('products', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->product?->getKey()),
            ],
            'variants' => ['required', 'array', 'min:1'],
            'variants.*.sku' => ['nullable', 'string', 'max:255'],
            'variants.*.price' => ['required', 'numeric', 'min:0'],
            'variants.*.compareAtPrice' => ['nullable', 'numeric', 'min:0'],
            'variants.*.quantity' => ['required', 'integer', 'min:0'],
            'variants.*.requiresShipping' => ['boolean'],
        ]);

        if (! $this->validateActiveVariantPricing() || ! $this->validateVariantSkus($store)) {
            return;
        }

        $product = DB::transaction(function () use ($store): Product {
            $productService = app(ProductService::class);

            if ($this->product === null) {
                $product = $productService->create($store, [
                    ...$this->productPayload(),
                    'options' => $this->optionPayload(),
                    'variants' => $this->variantPayload($store),
                ]);
            } else {
                $payload = $this->productPayload();
                $requestedStatus = ProductStatus::from((string) $payload['status']);
                unset($payload['status']);

                $product = $productService->update($this->product, $payload);
                $this->syncExistingVariants($product, $store);

                if ($requestedStatus !== $product->refresh()->status) {
                    $productService->transitionStatus($product, $requestedStatus);
                }
            }

            $product->collections()->sync($this->collectionIds);

            return $product->refresh();
        });

        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections']);
        $this->fillFromProduct($this->product);

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

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,


message: For discounts I’ll mirror the existing product/collection admin pattern: index plus create/edit form, store-scoped validation, rule JSON for minimums and product/collection targeting, and tests around listing, saving, and scoping.

phase: commentary


type: message

role: assistant

text: For discounts I’ll mirror the existing product/collection admin pattern: index plus create/edit form, store-scoped validation, rule JSON for minimums and product/collection targeting, and tests around listing, saving, and scoping.


cmd: php artisan make:livewire Admin/Discounts/Index --class --no-interaction


cmd: php artisan make:livewire Admin/Discounts/Form --class --no-interaction


cmd: php artisan make:test --pest Admin/DiscountManagementTest --no-interaction


output:
Chunk ID: 56ca8f
Wall time: 0.1493 seconds
Process exited with code 0
Original token count: 24
Output:

   INFO  Livewire component [app/Livewire/Admin/Discounts/Index.php] created successfully.


output:
Chunk ID: 82cc8a
Wall time: 0.1352 seconds
Process exited with code 0
Original token count: 24
Output:

   INFO  Livewire component [app/Livewire/Admin/Discounts/Form.php] created successfully.


output:
Chunk ID: cc1f8b
Wall time: 0.1337 seconds
Process exited with code 0
Original token count: 23
Output:

   INFO  Test [tests/Feature/Admin/DiscountManagementTest.php] created successfully.


command: php artisan make:test --pest Admin/DiscountManagementTest --no-interaction

type: unknown

aggregated_output: INFO  Test [tests/Feature/Admin/DiscountManagementTest.php] created successfully.

status: completed


command: php artisan make:livewire Admin/Discounts/Index --class --no-interaction

aggregated_output: INFO  Livewire component [app/Livewire/Admin/Discounts/Index.php] created successfully.


command: php artisan make:livewire Admin/Discounts/Form --class --no-interaction

aggregated_output: INFO  Livewire component [app/Livewire/Admin/Discounts/Form.php] created successfully.


step: Inspect discount admin spec and discount data model

step: Build discount routes, sidebar, list/form components

step: Add discount management Pest coverage

status: pending

step: Run formatter, affected/full tests, build, browser, progress, commit

explanation: The customer slice is committed; the active slice is now discount administration from Spec 03.


output: Plan updated


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Index.php
@@
 <?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
 {
-    public function render()
+    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');
+        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,
+        };
     }
 }
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Index.php

type: update

unified_diff:
@@ -4,3 +4,13 @@

+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;

@@ -8,5 +18,112 @@
 {
-    public function render()
+    use WithPagination;
+
+    #[Locked]
+    public int $storeId;
+
+    public string $storeCurrency = 'EUR';
+
+    public string $search = '';
+
+    public string $statusFilter = 'all';
+
+    public function mount(): void
     {
-        return view('livewire.admin.discounts.index');
+        $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: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Index.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Form.php
@@
 <?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 function render()
+    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');
+        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();
     }
 }
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Form.php

unified_diff:
@@ -4,2 +4,13 @@

+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;
@@ -8,5 +19,324 @@
 {
-    public function render()
+    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 view('livewire.admin.discounts.form');
+        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: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Form.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php
*** End Patch


stdout:
Success. Updated the following files:
D /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php

type: delete

content: {{-- Knowing is not enough; we must apply. Being willing is not enough; we must do. - Leonardo da Vinci --}}


output: {"output":"Success. Updated the following files:\nD /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php
+

+

+

+            <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() }}
+

*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php

type: add

content:
            <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: {"output":"Success. Updated the following files:\nA /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


cmd: rg -n "flux:switch|flux:radio|flux:checkbox" resources/views/livewire/admin resources/views/livewire/storefront -n


command: rg -n "flux:switch|flux:radio|flux:checkbox" resources/views/livewire/admin resources/views/livewire/storefront -n

type: search

cmd: rg -n 'flux:switch|flux:radio|flux:checkbox' resources/views/livewire/admin resources/views/livewire/storefront -n

query: flux:switch|flux:radio|flux:checkbox

path: admin

aggregated_output:
resources/views/livewire/storefront/collections/show.blade.php:22:                <flux:checkbox wire:model.live="inStock" label="In stock" />
resources/views/livewire/storefront/collections/show.blade.php:36:                            <flux:checkbox wire:model.live="types" value="{{ $type }}" :label="$type" wire:key="type-filter-{{ $type }}" />
resources/views/livewire/storefront/collections/show.blade.php:45:                            <flux:checkbox wire:model.live="vendors" value="{{ $vendor }}" :label="$vendor" wire:key="vendor-filter-{{ $vendor }}" />
resources/views/livewire/storefront/account/auth/login.blade.php:25:        <flux:checkbox wire:model="remember" :label="__('Remember me')" />
resources/views/livewire/storefront/account/auth/register.blade.php:41:        <flux:checkbox wire:model="marketing_opt_in" :label="__('Email me about new products and offers')" />
resources/views/livewire/admin/auth/login.blade.php:25:        <flux:checkbox wire:model="remember" :label="__('Remember me')" />
resources/views/livewire/storefront/checkout/show.blade.php:75:                    <flux:checkbox wire:model="billingSame" label="Billing address is the same" class="mt-5" />
resources/views/livewire/admin/products/form.blade.php:69:                                        <flux:checkbox wire:model="variants.{{ $index }}.requiresShipping" aria-label="Requires shipping" />
resources/views/livewire/admin/products/form.blade.php:127:                        <flux:checkbox wire:model="collectionIds" value="{{ $collection->getKey() }}" :label="$collection->title" wire:key="product-collection-{{ $collection->getKey() }}" />


output:
Chunk ID: 98bb0c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 411
Output:
resources/views/livewire/storefront/collections/show.blade.php:22:                <flux:checkbox wire:model.live="inStock" label="In stock" />
resources/views/livewire/storefront/collections/show.blade.php:36:                            <flux:checkbox wire:model.live="types" value="{{ $type }}" :label="$type" wire:key="type-filter-{{ $type }}" />
resources/views/livewire/storefront/collections/show.blade.php:45:                            <flux:checkbox wire:model.live="vendors" value="{{ $vendor }}" :label="$vendor" wire:key="vendor-filter-{{ $vendor }}" />
resources/views/livewire/storefront/account/auth/login.blade.php:25:        <flux:checkbox wire:model="remember" :label="__('Remember me')" />
resources/views/livewire/storefront/account/auth/register.blade.php:41:        <flux:checkbox wire:model="marketing_opt_in" :label="__('Email me about new products and offers')" />
resources/views/livewire/admin/auth/login.blade.php:25:        <flux:checkbox wire:model="remember" :label="__('Remember me')" />
resources/views/livewire/storefront/checkout/show.blade.php:75:                    <flux:checkbox wire:model="billingSame" label="Billing address is the same" class="mt-5" />
resources/views/livewire/admin/products/form.blade.php:69:                                        <flux:checkbox wire:model="variants.{{ $index }}.requiresShipping" aria-label="Requires shipping" />
resources/views/livewire/admin/products/form.blade.php:127:                        <flux:checkbox wire:model="collectionIds" value="{{ $collection->getKey() }}" :label="$collection->title" wire:key="product-collection-{{ $collection->getKey() }}" />


status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php
*** End Patch


stdout:
Success. Updated the following files:
D /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php


output: {"output":"Success. Updated the following files:\nD /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php
+

+

+

+            <flux:heading size="xl">{{ $isEditing ? 'Edit discount' : 'Create discount' }}</flux:heading>
+            <flux:text class="mt-1">Configure the promotion type, value, eligibility, limits, and dates.</flux:text>
+

+
+        <flux:button :href="route('admin.discounts.index')" wire:navigate variant="filled" icon="arrow-left">
+            Discounts
+        </flux:button>
+

+
+    <form wire:submit="save" class="space-y-6">
+

+            <flux:radio.group wire:model.live="type" label="Type" class="grid gap-3 sm:grid-cols-2">
+                <flux:radio value="code" label="Discount code" description="Customers enter a code at checkout" />
+                <flux:radio value="automatic" label="Automatic discount" description="Applied automatically at checkout" />
+            </flux:radio.group>
+            <flux:error name="type" />
+

+
+        @if ($type === 'code')
+

+                <flux:heading size="lg">Code</flux:heading>
+

+

+                        <flux:input wire:model="code" label="Discount code" placeholder="SUMMER20" />
+                        <flux:error name="code" />
+

+

+                        <flux:button type="button" wire:click="generateCode" variant="filled">Generate</flux:button>
+

+

+

+        @endif
+
+

+            <flux:heading size="lg">Value</flux:heading>
+
+

+                <flux:radio.group wire:model.live="valueType" label="Value type" class="grid gap-3 sm:grid-cols-3">
+                    <flux:radio value="percent" label="Percentage" />
+                    <flux:radio value="fixed" label="Fixed amount" />
+                    <flux:radio value="free_shipping" label="Free shipping" />
+                </flux:radio.group>
+
+                @if ($valueType !== 'free_shipping')
+

+                        <flux:input wire:model="valueAmount" type="number" step="0.01" min="0" label="{{ $valueType === 'percent' ? 'Percentage' : 'Amount' }}" />
+                        <flux:error name="valueAmount" />
+

+                @endif
+

+

+
+

+            <flux:heading size="lg">Conditions</flux:heading>
+
+

+

+                    <flux:input wire:model="minimumPurchaseAmount" type="number" step="0.01" min="0" label="Minimum purchase amount" placeholder="0.00" />
+                    <flux:error name="minimumPurchaseAmount" />
+

+
+

+

+                        <flux:input wire:model.live.debounce.300ms="productSearch" icon="magnifying-glass" label="Specific products" placeholder="Search products..." />
+
+                        @if ($productResults->isNotEmpty())
+

+                                @foreach ($productResults as $product)
+                                    <button type="button" wire:click="addProduct({{ $product->getKey() }})" wire:key="discount-product-result-{{ $product->getKey() }}" class="block w-full px-3 py-2 text-left text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800">
+                                        {{ $product->title }}
+                                    </button>
+                                @endforeach
+

+                        @endif
+
+

+                            @foreach ($selectedProducts as $product)
+
getKey() }}" class="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
+                                    {{ $product->title }}
+                                    <flux:button type="button" wire:click="removeProduct({{ $product->getKey() }})" size="sm" variant="ghost" icon="x-mark" aria-label="Remove {{ $product->title }}" />
+

+                            @endforeach
+

+

+
+

+                        <flux:input wire:model.live.debounce.300ms="collectionSearch" icon="magnifying-glass" label="Specific collections" placeholder="Search collections..." />
+
+                        @if ($collectionResults->isNotEmpty())
+

+                                @foreach ($collectionResults as $collection)
+                                    <button type="button" wire:click="addCollection({{ $collection->getKey() }})" wire:key="discount-collection-result-{{ $collection->getKey() }}" class="block w-full px-3 py-2 text-left text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800">
+                                        {{ $collection->title }}
+                                    </button>
+                                @endforeach
+

+                        @endif
+
+

+                            @foreach ($selectedCollections as $collection)
+
getKey() }}" class="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
+                                    {{ $collection->title }}
+                                    <flux:button type="button" wire:click="removeCollection({{ $collection->getKey() }})" size="sm" variant="ghost" icon="x-mark" aria-label="Remove {{ $collection->title }}" />
+

+                            @endforeach
+

+

+

+

+

+
+

+            <flux:heading size="lg">Usage limits</flux:heading>
+
+

+                <flux:input wire:model="usageLimit" type="number" min="1" label="Total usage limit" placeholder="Unlimited" />
+

+                    <flux:checkbox wire:model="onePerCustomer" label="Limit to one use per customer" />
+

+

+            <flux:error name="usageLimit" />
+

+
+

+            <flux:heading size="lg">Active dates</flux:heading>
+
+

+                <flux:input wire:model="startsAt" type="datetime-local" label="Start date" />
+                <flux:input wire:model="endsAt" type="datetime-local" label="End date" />
+

+

+                <flux:error name="startsAt" />
+                <flux:error name="endsAt" />
+

+

+
+

+            <flux:checkbox wire:model="isActive" label="{{ $isActive ? 'Active' : 'Disabled' }}" />
+

+
+

+

+                <flux:button :href="route('admin.discounts.index')" wire:navigate variant="filled">Discard</flux:button>
+                <flux:button type="submit" variant="primary">Save</flux:button>
+

+

+    </form>
+

*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php

content:
            <flux:heading size="xl">{{ $isEditing ? 'Edit discount' : 'Create discount' }}</flux:heading>
            <flux:text class="mt-1">Configure the promotion type, value, eligibility, limits, and dates.</flux:text>

        <flux:button :href="route('admin.discounts.index')" wire:navigate variant="filled" icon="arrow-left">
            Discounts
        </flux:button>

    <form wire:submit="save" class="space-y-6">

            <flux:radio.group wire:model.live="type" label="Type" class="grid gap-3 sm:grid-cols-2">
                <flux:radio value="code" label="Discount code" description="Customers enter a code at checkout" />
                <flux:radio value="automatic" label="Automatic discount" description="Applied automatically at checkout" />
            </flux:radio.group>
            <flux:error name="type" />

        @if ($type === 'code')

                <flux:heading size="lg">Code</flux:heading>

                        <flux:input wire:model="code" label="Discount code" placeholder="SUMMER20" />
                        <flux:error name="code" />

                        <flux:button type="button" wire:click="generateCode" variant="filled">Generate</flux:button>

        @endif

            <flux:heading size="lg">Value</flux:heading>

                <flux:radio.group wire:model.live="valueType" label="Value type" class="grid gap-3 sm:grid-cols-3">
                    <flux:radio value="percent" label="Percentage" />
                    <flux:radio value="fixed" label="Fixed amount" />
                    <flux:radio value="free_shipping" label="Free shipping" />
                </flux:radio.group>

                @if ($valueType !== 'free_shipping')

                        <flux:input wire:model="valueAmount" type="number" step="0.01" min="0" label="{{ $valueType === 'percent' ? 'Percentage' : 'Amount' }}" />
                        <flux:error name="valueAmount" />

                @endif

            <flux:heading size="lg">Conditions</flux:heading>

                    <flux:input wire:model="minimumPurchaseAmount" type="number" step="0.01" min="0" label="Minimum purchase amount" placeholder="0.00" />
                    <flux:error name="minimumPurchaseAmount" />

                        <flux:input wire:model.live.debounce.300ms="productSearch" icon="magnifying-glass" label="Specific products" placeholder="Search products..." />

                        @if ($productResults->isNotEmpty())

                                @foreach ($productResults as $product)
                                    <button type="button" wire:click="addProduct({{ $product->getKey() }})" wire:key="discount-product-result-{{ $product->getKey() }}" class="block w-full px-3 py-2 text-left text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800">
                                        {{ $product->title }}
                                    </button>
                                @endforeach

                        @endif

                            @foreach ($selectedProducts as $product)

getKey() }}" class="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
                                    {{ $product->title }}
                                    <flux:button type="button" wire:click="removeProduct({{ $product->getKey() }})" size="sm" variant="ghost" icon="x-mark" aria-label="Remove {{ $product->title }}" />

                            @endforeach

                        <flux:input wire:model.live.debounce.300ms="collectionSearch" icon="magnifying-glass" label="Specific collections" placeholder="Search collections..." />

                        @if ($collectionResults->isNotEmpty())

                                @foreach ($collectionResults as $collection)
                                    <button type="button" wire:click="addCollection({{ $collection->getKey() }})" wire:key="discount-collection-result-{{ $collection->getKey() }}" class="block w-full px-3 py-2 text-left text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800">
                                        {{ $collection->title }}
                                    </button>
                                @endforeach

                        @endif

                            @foreach ($selectedCollections as $collection)

getKey() }}" class="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
                                    {{ $collection->title }}
                                    <flux:button type="button" wire:click="removeCollection({{ $collection->getKey() }})" size="sm" variant="ghost" icon="x-mark" aria-label="Remove {{ $collection->title }}" />

                            @endforeach

            <flux:heading size="lg">Usage limits</flux:heading>

                <flux:input wire:model="usageLimit" type="number" min="1" label="Total usage limit" placeholder="Unlimited" />

                    <flux:checkbox wire:model="onePerCustomer" label="Limit to one use per customer" />

            <flux:error name="usageLimit" />

            <flux:heading size="lg">Active dates</flux:heading>

                <flux:input wire:model="startsAt" type="datetime-local" label="Start date" />
                <flux:input wire:model="endsAt" type="datetime-local" label="End date" />

                <flux:error name="startsAt" />
                <flux:error name="endsAt" />

            <flux:checkbox wire:model="isActive" label="{{ $isActive ? 'Active' : 'Disabled' }}" />

                <flux:button :href="route('admin.discounts.index')" wire:navigate variant="filled">Discard</flux:button>
                <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


output: {"output":"Success. Updated the following files:\nA /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/routes/web.php
@@
 use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
 use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
 use App\Livewire\Admin\Dashboard as AdminDashboard;
+use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
+use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
 use App\Livewire\Admin\Inventory\Index as AdminInventoryIndex;
@@
     Route::livewire('customers', AdminCustomersIndex::class)->name('customers.index');
     Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
+    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
+    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
+    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
     Route::livewire('collections', AdminCollectionsIndex::class)->name('collections.index');
*** End Patch


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

unified_diff:
@@ -8,2 +8,4 @@
 use App\Livewire\Admin\Dashboard as AdminDashboard;
+use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
+use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
 use App\Livewire\Admin\Inventory\Index as AdminInventoryIndex;
@@ -64,2 +66,5 @@
     Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
+    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
+    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
+    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
     Route::livewire('collections', AdminCollectionsIndex::class)->name('collections.index');


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


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/layouts/app/sidebar.blade.php
@@
                     <flux:sidebar.item icon="users" :href="route('admin.customers.index')" :current="request()->routeIs('admin.customers.*')" wire:navigate>
                         {{ __('Customers') }}
                     </flux:sidebar.item>
+
+                    <flux:sidebar.item icon="tag" :href="route('admin.discounts.index')" :current="request()->routeIs('admin.discounts.*')" wire:navigate>
+                        {{ __('Discounts') }}
+                    </flux:sidebar.item>
                 </flux:sidebar.group>
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/resources/views/layouts/app/sidebar.blade.php

unified_diff:
@@ -41,2 +41,6 @@
                     </flux:sidebar.item>
+
+                    <flux:sidebar.item icon="tag" :href="route('admin.discounts.index')" :current="request()->routeIs('admin.discounts.*')" wire:navigate>
+                        {{ __('Discounts') }}
+                    </flux:sidebar.item>
                 </flux:sidebar.group>


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/resources/views/layouts/app/sidebar.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


message: The discount UI is wired. I’m adding focused tests for route scoping, filtering, form creation with rule JSON, editing, and cross-store rejection.


text: The discount UI is wired. I’m adding focused tests for route scoping, filtering, form creation with rule JSON, editing, and cross-store rejection.


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Admin/DiscountManagementTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+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;

-    $response->assertStatus(200);
+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);
 });
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Feature/Admin/DiscountManagementTest.php

unified_diff:
@@ -2,6 +2,157 @@

-test('example', function () {
-    $response = $this->get('/');
+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();
+}

-    $response->assertStatus(200);
+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: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Feature/Admin/DiscountManagementTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}