git diff -- app/Providers/AppServiceProvider.php routes/web.php resources/views/layouts/app/sidebar.blade.php

diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 4d7eb734..7df56117 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -4,6 +4,8 @@

 use App\Auth\CustomerUserProvider;
 use App\Contracts\PaymentProvider;
+use App\Http\Middleware\CheckStoreRole;
+use App\Http\Middleware\ResolveStore;
 use App\Models\Store;
 use App\Services\NavigationService;
 use App\Services\Payments\MockPaymentProvider;
@@ -20,6 +22,7 @@
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Validation\Rules\Password;
 use Illuminate\View\View as ViewInstance;
+use Livewire\Livewire;

 class AppServiceProvider extends ServiceProvider
 {
@@ -44,6 +47,7 @@ public function register(): void
     public function boot(): void
     {
         $this->configureDefaults();
+        $this->configureLivewireMiddleware();
         $this->configureStorefrontViewData();
     }

@@ -116,4 +120,12 @@ protected function configureStorefrontViewData(): void
             ]);
         });
     }
+
+    protected function configureLivewireMiddleware(): void
+    {
+        Livewire::addPersistentMiddleware([
+            ResolveStore::class,
+            CheckStoreRole::class,
+        ]);
+    }
 }
diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php
index aaa4c7ed..0ca8f8a3 100644
--- a/resources/views/layouts/app/sidebar.blade.php
+++ b/resources/views/layouts/app/sidebar.blade.php
@@ -39,6 +39,10 @@
                     <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>
             </flux:sidebar.nav>

diff --git a/routes/web.php b/routes/web.php
index cc917482..34c8aca8 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -6,6 +6,8 @@
 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;
 use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
 use App\Livewire\Admin\Orders\Show as AdminOrderShow;
@@ -62,6 +64,9 @@
     Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
     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');
     Route::livewire('collections/create', AdminCollectionForm::class)->name('collections.create');
     Route::livewire('collections/{collection}/edit', AdminCollectionForm::class)->name('collections.edit');


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

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 ?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();
    }
}


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

DiscountManagementTest.php

<?php

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\ResolveStore;
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('livewire persists store middleware for admin action requests', function (): void {
    expect(Livewire::getPersistentMiddleware())
        ->toContain(ResolveStore::class)
        ->toContain(CheckStoreRole::class);
});

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


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

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>

    @if (session('status'))
        <flux:callout color="green" icon="check-circle">{{ session('status') }}</flux:callout>
    @endif

    <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="ghost">Discard</flux:button>
                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
                    Save
                    Saving...
                </flux:button>

    </form>


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

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


Output:
Chunk ID: ab57ab
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 895
Output:
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 4d7eb734..7df56117 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -4,6 +4,8 @@

 use App\Auth\CustomerUserProvider;
 use App\Contracts\PaymentProvider;
+use App\Http\Middleware\CheckStoreRole;
+use App\Http\Middleware\ResolveStore;
 use App\Models\Store;
 use App\Services\NavigationService;
 use App\Services\Payments\MockPaymentProvider;
@@ -20,6 +22,7 @@
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Validation\Rules\Password;
 use Illuminate\View\View as ViewInstance;
+use Livewire\Livewire;

 class AppServiceProvider extends ServiceProvider
 {
@@ -44,6 +47,7 @@ public function register(): void
     public function boot(): void
     {
         $this->configureDefaults();
+        $this->configureLivewireMiddleware();
         $this->configureStorefrontViewData();
     }

@@ -116,4 +120,12 @@ protected function configureStorefrontViewData(): void
             ]);
         });
     }
+
+    protected function configureLivewireMiddleware(): void
+    {
+        Livewire::addPersistentMiddleware([
+            ResolveStore::class,
+            CheckStoreRole::class,
+        ]);
+    }
 }
diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php
index aaa4c7ed..0ca8f8a3 100644
--- a/resources/views/layouts/app/sidebar.blade.php
+++ b/resources/views/layouts/app/sidebar.blade.php
@@ -39,6 +39,10 @@
                     <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>
             </flux:sidebar.nav>

diff --git a/routes/web.php b/routes/web.php
index cc917482..34c8aca8 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -6,6 +6,8 @@
 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;
 use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
 use App\Livewire\Admin\Orders\Show as AdminOrderShow;
@@ -62,6 +64,9 @@
     Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
     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');
     Route::livewire('collections/create', AdminCollectionForm::class)->name('collections.create');
     Route::livewire('collections/{collection}/edit', AdminCollectionForm::class)->name('collections.edit');


Output:
Chunk ID: 3cbfcf
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 947
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 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->value)
                ->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->value)->orWhere('ends_at', '<', now());
            }),
            'scheduled' => $query->where('starts_at', '>', now()),
            default => null,
        };
    }
}


Output:
Chunk ID: 9abe0a
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: a63e7e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1458
Output:
<?php

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\ResolveStore;
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('livewire persists store middleware for admin action requests', function (): void {
    expect(Livewire::getPersistentMiddleware())
        ->toContain(ResolveStore::class)
        ->toContain(CheckStoreRole::class);
});

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: 62c883
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1223
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: f7afd4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2304
Output:

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

    @if (session('status'))
        <flux:callout color="green" icon="check-circle">{{ session('status') }}</flux:callout>
    @endif

    <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="ghost">Discard</flux:button>
                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
                    Save
                    Saving...
                </flux:button>

    </form>


sed -n '260,290p' vendor/livewire/livewire/src/LivewireManager.php && sed -n '55,80p' vendor/livewire/livewire/src/Mechanisms/PersistentMiddleware/PersistentMiddleware.php


sed -n '260,290p' vendor/livewire/livewire/src/LivewireManager.php && sed -n '55,80p' vendor/livewire/livewire/src/Mechanisms/PersistentMiddleware/PersistentMiddleware.php

PersistentMiddleware.php

         return $this;
    }

    function isRunningServerless()
    {
        return in_array($_ENV['SERVER_SOFTWARE'] ?? null, [
            'vapor',
            'bref',
        ]);
    }

    function addPersistentMiddleware($middleware)
    {
        app(PersistentMiddleware::class)->addPersistentMiddleware($middleware);
    }

    function setPersistentMiddleware($middleware)
    {
        app(PersistentMiddleware::class)->setPersistentMiddleware($middleware);
    }

    function getPersistentMiddleware()
    {
        return app(PersistentMiddleware::class)->getPersistentMiddleware();
    }

    function zap()
    {
        return app('livewire.zap');
    }
            $this->resolvedRouteModels = [];
        });
    }

    function addPersistentMiddleware($middleware)
    {
        static::$persistentMiddleware = Router::uniqueMiddleware(array_merge(static::$persistentMiddleware, (array) $middleware));
    }

    function setPersistentMiddleware($middleware)
    {
        static::$persistentMiddleware = Router::uniqueMiddleware((array) $middleware);
    }

    function getPersistentMiddleware()
    {
        return static::$persistentMiddleware;
    }

    function getResolvedRouteModel($class, $key)
    {
        return $this->resolvedRouteModels[$class.':'.$key] ?? null;
    }

    protected function extractPathAndMethodFromRequest()
    {


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

         return $this;
    }

    function isRunningServerless()
    {
        return in_array($_ENV['SERVER_SOFTWARE'] ?? null, [
            'vapor',
            'bref',
        ]);
    }

    function addPersistentMiddleware($middleware)
    {
        app(PersistentMiddleware::class)->addPersistentMiddleware($middleware);
    }

    function setPersistentMiddleware($middleware)
    {
        app(PersistentMiddleware::class)->setPersistentMiddleware($middleware);
    }

    function getPersistentMiddleware()
    {
        return app(PersistentMiddleware::class)->getPersistentMiddleware();
    }

    function zap()
    {
        return app('livewire.zap');
    }
            $this->resolvedRouteModels = [];
        });
    }

    function addPersistentMiddleware($middleware)
    {
        static::$persistentMiddleware = Router::uniqueMiddleware(array_merge(static::$persistentMiddleware, (array) $middleware));
    }

    function setPersistentMiddleware($middleware)
    {
        static::$persistentMiddleware = Router::uniqueMiddleware((array) $middleware);
    }

    function getPersistentMiddleware()
    {
        return static::$persistentMiddleware;
    }

    function getResolvedRouteModel($class, $key)
    {
        return $this->resolvedRouteModels[$class.':'.$key] ?? null;
    }

    protected function extractPathAndMethodFromRequest()
    {


I’m adding one small route assertion for the edit URL, so the HTTP route coverage matches the create/list coverage instead of relying only on direct component testing.


Input:
*** Begin Patch
*** Update File: tests/Feature/Admin/DiscountManagementTest.php
@@
 test('admin discount routes require authentication and render store scoped discounts', function (): void {
     $store = adminDiscountManagementStore();
-    Discount::factory()->create([
+    $discount = Discount::factory()->create([
         'store_id' => $store->getKey(),
         'code' => 'SAVE20',
         'value_type' => DiscountValueType::Percent,
@@
         ->get('/admin/discounts/create')
         ->assertSuccessful()
         ->assertSee('Create discount');
+
+    $this->actingAs(adminDiscountManagementUser())
+        ->withSession(['current_store_id' => $store->getKey()])
+        ->get('/admin/discounts/'.$discount->getKey().'/edit')
+        ->assertSuccessful()
+        ->assertSee('SAVE20');
 });
*** End Patch


Success. Updated the following files:
M tests/Feature/Admin/DiscountManagementTest.php

@@ -46,3 +46,3 @@
     $store = adminDiscountManagementStore();
-    Discount::factory()->create([
+    $discount = Discount::factory()->create([
         'store_id' => $store->getKey(),
@@ -75,2 +75,8 @@
         ->assertSee('Create discount');
+
+    $this->actingAs(adminDiscountManagementUser())
+        ->withSession(['current_store_id' => $store->getKey()])
+        ->get('/admin/discounts/'.$discount->getKey().'/edit')
+        ->assertSuccessful()
+        ->assertSee('SAVE20');
 });


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


Output:
Chunk ID: 4d2868
Wall time: 0.2238 seconds
Process exited with code 0
Original token count: 5
Output:
{"result":"pass"}


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


php artisan test --compact tests/Feature/Admin/DiscountManagementTest.php


Output:
Chunk ID: f593cb
Wall time: 1.0015 seconds
Process running with session ID 48146
Original token count: 0
Output:


php artisan test --compact tests/Feature/Admin/DiscountManagementTest.php

  .....

  Tests:    5 passed (33 assertions)
  Duration: 1.95s


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

  .....

  Tests:    5 passed (33 assertions)
  Duration: 1.95s


The focused discount tests still pass with 5 tests and 33 assertions. I’m rerunning the full suite once more so the final evidence matches the current test tree exactly.


Output:
Chunk ID: 1f4c4d
Wall time: 1.0015 seconds
Process running with session ID 56857
Original token count: 1
Output:

  .


Output:
Chunk ID: 464af6
Wall time: 5.0012 seconds
Process running with session ID 56857
Original token count: 21
Output:
...........................................................................
  ....


Output:
Chunk ID: 1e2446
Wall time: 2.5923 seconds
Process exited with code 0
Original token count: 31
Output:
..............................................................

  Tests:    142 passed (700 assertions)
  Duration: 19.21s


The current full suite is clean: 142 tests, 700 assertions. I’m updating `specs/progress.md` with the discount slice, verification evidence, and the next remaining admin gaps.


/bin/zsh
-lc
sed -n '1,260p' specs/progress.md

# Shop Build Progress

## Objective

Build a complete, self-contained Laravel shop system from `specs/*`, with implemented acceptance criteria mapped to evidence, passing automated checks, verified browser flows, independent QA, and meaningful commits.

## Current Status

- Status: in progress
- Active slice: Phase 6 - admin discounts/content/settings surfaces
- Started from: Laravel Livewire/Fortify starter kit with no shop domain tables or models present
- Last updated: 2026-05-04

## Phased Plan

1. Foundation: SQLite config, tenancy tables/models/factories/seeders, store resolution middleware, store scoping, admin/customer auth foundations, policies, and Phase 1 tests.
2. Catalog: product, variant, inventory, collection, media schema/models/services, admin catalog CRUD, storefront browsing, and tests.
3. Storefront theme/layout: theme/page/navigation data, storefront layout, product/collection/search/page views, and smoke browser verification.
4. Cart/checkout/pricing: cart API/UI, discount, shipping, tax, checkout state machine, and tests.
5. Payments/orders/fulfillment: mock PSP, order creation, refunds, fulfillments, customer account order views, and tests.
6. Admin panel: dashboard and all admin list/form/detail pages from Spec 03, with Livewire/Flux interactions.
7. Search/analytics/apps/webhooks: SQLite search, event aggregation, developer token/webhook UI, delivery jobs, and tests.
8. Final verification: full Pest suite, Pint, frontend build, Playwright MCP customer/admin flows, independent QA, fixes, and completion audit.

## Acceptance Checklist

| Area | Criteria Source | Status | Evidence |
| --- | --- | --- | --- |
| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, Phase 4 cart/checkout/pricing tables, and Phase 5 order/payment/fulfillment tables are implemented: customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Search/analytics/app tables are still missing. |
| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, and token-gated order lookup under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. Storefront search/suggest, analytics, app/webhook APIs, and broader admin REST surfaces are still missing. |
| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog and operations shell now includes dashboard KPI/date-range reporting, orders-over-time bars, top-product summaries, funnel counters, product index/form, collection index/form, inventory list, order index/detail, order search/status filters, bank-transfer payment confirmation, refund creation, fulfillment creation, shipment status transitions, customer index/detail, customer order history, customer address create/edit/delete/default actions, and order-to-customer links with auth protection and store scoping. Discount/content/settings admin pages are still missing. |
| Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, search, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, and customer order list/detail views render seeded and runtime data. |
| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-g…25943 chars truncated…Service::completeCheckout()` so failed card payments can release reservations and return customers to payment selection.
- Order confirmation access is allowed for the authenticated owning customer or for the session that just placed the order via `last_order_id`.
- Customer account order list/detail routes use the `customer` guard and reload orders through explicit store/customer constraints.
- The cart REST API exposes `cart_version` as the public optimistic concurrency field while the service layer keeps its `expectedVersion` argument; `expected_version` remains accepted as a compatibility alias in API requests.
- Cart page discount codes are validated with `DiscountService`, saved in session, and applied to the checkout when the customer proceeds.
- `orders.checkout_id` is intentionally added beyond the original schema table to enforce idempotent checkout completion without duplicate orders.
- Failed card payments release reserved inventory and move the checkout back to `shipping_selected` so customers can retry payment selection.
- Bank transfer order completion creates a pending order and payment while keeping inventory reserved until the later admin payment-confirmation flow.
- Inventory commit/release/restock locks by primary key without the current-store global scope because scheduled jobs can operate across stores in one run.
- Admin order Livewire components persist locked `storeId`/`orderId` state and reload orders without global scopes but with explicit store constraints before each service-backed action.
- Admin order actions deliberately delegate to `OrderService`, `RefundService`, and `FulfillmentService` so UI validation cannot bypass payment, refund, fulfillment, or inventory guards.
- Storefront order lookup uses a deterministic HMAC access token derived from store/order identity so confirmation links can be token-gated without adding a new runtime column.
- Admin order API routes currently use the existing session `auth` middleware and store-user membership checks because Sanctum is not installed yet.
- Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; visitor counts remain zero until the analytics event tables are implemented.
- Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.

## Open Issues

- Composer does not currently include Sanctum, while Spec 06 requires Sanctum personal access tokens. This will need either an approved dependency addition or a documented compatible alternative before final completion.
- The Herd app URL in the goal is `http://shop.test/`, while the E2E spec also references `http://acme-fashion.test`; domain handling must support seeded store domains and Herd verification.
- Phase 1 still lacks admin/customer password reset at the spec paths and a custom customer password reset token repository that scopes by store_id.
- Policy classes for later resources use generic object parameters until the concrete catalog/order/content models exist.
- `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
- Phase 2 media upload/admin UI is still missing, even though the media schema/job layer exists.
- Product options can be displayed and edited as text in the admin form, but full option matrix generation and option-value reassignment UI are not complete.
- Discount and theme/page/navigation admin management UI is still missing even though the relevant data layer, seeders, services, and storefront/checkout consumption exist.
- Storefront search/suggest, analytics event capture, app/webhook APIs, and broader admin REST endpoints outside order management are still missing.
- Automated browser tests from Spec 08 are still missing; current browser coverage is manual Playwright MCP smoke verification.
- SQLite enum/check constraints from the schema spec are not yet explicitly enforced as database `CHECK` constraints; enum validation is currently enforced through casts/services/model invariants.
- Product media processing now validates and resizes image uploads with GD and cleans predictable generated paths, but browser/admin upload UI is still pending.

## Completion Summary

Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer management, and order API surfaces are implemented, with known auth/token, media UI, discount/content/settings admin UI, search/analytics/app/webhook API, browser-test, and database CHECK constraint gaps tracked above.


/bin/zsh
-lc
sed -n '260,560p' specs/progress.md


/bin/zsh
-lc
sed -n '560,920p' specs/progress.md