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

<?php

use App\Models\Collection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;

uses(RefreshDatabase::class);

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

function adminDiscountApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminDiscountApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Discount integration', $abilities);
}

test('admin discount api lists creates updates and deletes discounts', function (): void {
    $store = adminDiscountApiStore();
    $product = Product::factory()->withDefaultVariant()->create(['store_id' => $store->getKey()]);
    $collection = Collection::factory()->create(['store_id' => $store->getKey()]);
    $user = adminDiscountApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts?type=code&status=active")
        ->assertOk();

    $createResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/discounts", [
            'type' => 'code',
            'code' => 'api20',
            'value_type' => 'percent',
            'value_amount' => 20,
            'starts_at' => now()->subHour()->toIso8601String(),
            'ends_at' => now()->addMonth()->toIso8601String(),
            'usage_limit' => 50,
            'rules_json' => [
                'minimum_purchase_amount' => 5000,
                'applicable_product_ids' => [$product->getKey()],
                'applicable_collection_ids' => [$collection->getKey()],
                'customer_eligibility' => 'all',
                'once_per_customer' => true,
            ],
        ])
        ->assertCreated()
        ->assertJsonPath('data.code', 'API20')
        ->assertJsonPath('data.rules_json.min_purchase_amount', 5000)
        ->assertJsonPath('data.rules_json.one_per_customer', true);

    $discount = Discount::withoutGlobalScopes()->findOrFail($createResponse->json('data.id'));

    expect($discount->rules_json['applicable_product_ids'])->toBe([$product->getKey()])
        ->and($discount->rules_json['applicable_collection_ids'])->toBe([$collection->getKey()]);

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}", [
            'value_type' => 'fixed',
            'value_amount' => 750,
            'usage_limit' => 25,
            'status' => 'disabled',
        ])
        ->assertOk()
        ->assertJsonPath('data.value_type', 'fixed')
        ->assertJsonPath('data.value_amount', 750)
        ->assertJsonPath('data.status', 'disabled');

    $this->actingAs($user)
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertOk()
        ->assertJsonPath('message', 'Discount deleted');

    expect(Discount::withoutGlobalScopes()->whereKey($discount->getKey())->exists())->toBeFalse();
});

test('admin discount api enforces token abilities and store scope', function (): void {
    $store = adminDiscountApiStore();
    $otherStore = Store::factory()->create();
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'TOKEN25',
    ]);
    $readToken = adminDiscountApiToken($store, ['read-discounts']);
    $writeToken = adminDiscountApiToken($store, ['write-discounts']);
    $otherStoreToken = adminDiscountApiToken($otherStore, ['read-discounts']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts?type=code")
        ->assertOk()
        ->assertJsonFragment(['code' => 'TOKEN25']);

    $this->withToken($readToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertOk();
});

test('admin discount api validates code dates and scoped rule resources', function (): void {
    $store = adminDiscountApiStore();
    $existing = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'EXISTING',
    ]);
    $otherStoreProduct = Product::factory()
        ->withDefaultVariant()
        ->create(['store_id' => Store::factory()->create()->getKey()]);

    $this->actingAs(adminDiscountApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/discounts", [
            'type' => 'code',
            'code' => Str::lower($existing->code),
            'value_type' => 'percent',
            'value_amount' => 101,
            'starts_at' => now()->addDay()->toIso8601String(),
            'ends_at' => now()->subDay()->toIso8601String(),
            'rules_json' => [
                'applicable_product_ids' => [$otherStoreProduct->getKey()],
            ],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['code', 'value_amount', 'ends_at', 'rules_json.applicable_product_ids.0']);
});


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

use App\Models\Collection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;

uses(RefreshDatabase::class);

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

function adminDiscountApiStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminDiscountApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Discount integration', $abilities);
}

test('admin discount api lists creates updates and deletes discounts', function (): void {
    $store = adminDiscountApiStore();
    $product = Product::factory()->withDefaultVariant()->create(['store_id' => $store->getKey()]);
    $collection = Collection::factory()->create(['store_id' => $store->getKey()]);
    $user = adminDiscountApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts?type=code&status=active")
        ->assertOk();

    $createResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/discounts", [
            'type' => 'code',
            'code' => 'api20',
            'value_type' => 'percent',
            'value_amount' => 20,
            'starts_at' => now()->subHour()->toIso8601String(),
            'ends_at' => now()->addMonth()->toIso8601String(),
            'usage_limit' => 50,
            'rules_json' => [
                'minimum_purchase_amount' => 5000,
                'applicable_product_ids' => [$product->getKey()],
                'applicable_collection_ids' => [$collection->getKey()],
                'customer_eligibility' => 'all',
                'once_per_customer' => true,
            ],
        ])
        ->assertCreated()
        ->assertJsonPath('data.code', 'API20')
        ->assertJsonPath('data.rules_json.min_purchase_amount', 5000)
        ->assertJsonPath('data.rules_json.one_per_customer', true);

    $discount = Discount::withoutGlobalScopes()->findOrFail($createResponse->json('data.id'));

    expect($discount->rules_json['applicable_product_ids'])->toBe([$product->getKey()])
        ->and($discount->rules_json['applicable_collection_ids'])->toBe([$collection->getKey()]);

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}", [
            'value_type' => 'fixed',
            'value_amount' => 750,
            'usage_limit' => 25,
            'status' => 'disabled',
        ])
        ->assertOk()
        ->assertJsonPath('data.value_type', 'fixed')
        ->assertJsonPath('data.value_amount', 750)
        ->assertJsonPath('data.status', 'disabled');

    $this->actingAs($user)
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertOk()
        ->assertJsonPath('message', 'Discount deleted');

    expect(Discount::withoutGlobalScopes()->whereKey($discount->getKey())->exists())->toBeFalse();
});

test('admin discount api enforces token abilities and store scope', function (): void {
    $store = adminDiscountApiStore();
    $otherStore = Store::factory()->create();
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'TOKEN25',
    ]);
    $readToken = adminDiscountApiToken($store, ['read-discounts']);
    $writeToken = adminDiscountApiToken($store, ['write-discounts']);
    $otherStoreToken = adminDiscountApiToken($otherStore, ['read-discounts']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts?type=code")
        ->assertOk()
        ->assertJsonFragment(['code' => 'TOKEN25']);

    $this->withToken($readToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/discounts")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/discounts/{$discount->getKey()}")
        ->assertOk();
});

test('admin discount api validates code dates and scoped rule resources', function (): void {
    $store = adminDiscountApiStore();
    $existing = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'EXISTING',
    ]);
    $otherStoreProduct = Product::factory()
        ->withDefaultVariant()
        ->create(['store_id' => Store::factory()->create()->getKey()]);

    $this->actingAs(adminDiscountApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/discounts", [
            'type' => 'code',
            'code' => Str::lower($existing->code),
            'value_type' => 'percent',
            'value_amount' => 101,
            'starts_at' => now()->addDay()->toIso8601String(),
            'ends_at' => now()->subDay()->toIso8601String(),
            'rules_json' => [
                'applicable_product_ids' => [$otherStoreProduct->getKey()],
            ],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['code', 'value_amount', 'ends_at', 'rules_json.applicable_product_ids.0']);
});


git diff -- routes/api.php specs/progress.md

diff --git a/routes/api.php b/routes/api.php
index a197d944..a0167e4e 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -2,6 +2,7 @@

 use App\Http\Controllers\Api\Admin\V1\CollectionController as AdminCollectionController;
 use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
+use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
 use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
 use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
 use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
@@ -73,6 +74,16 @@
             Route::delete('collections/{collection}', [AdminCollectionController::class, 'destroy'])->name('collections.destroy');
         });

+        Route::middleware('admin.api:read-discounts')->group(function (): void {
+            Route::get('discounts', [AdminDiscountController::class, 'index'])->name('discounts.index');
+        });
+
+        Route::middleware('admin.api:write-discounts')->group(function (): void {
+            Route::post('discounts', [AdminDiscountController::class, 'store'])->name('discounts.store');
+            Route::put('discounts/{discount}', [AdminDiscountController::class, 'update'])->name('discounts.update');
+            Route::delete('discounts/{discount}', [AdminDiscountController::class, 'destroy'])->name('discounts.destroy');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {
             Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
             Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
diff --git a/specs/progress.md b/specs/progress.md
index f684edc7..d247aedd 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -27,7 +27,7 @@ ## 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 including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/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}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. Write endpoints for remaining non-order admin REST surfaces are still missing. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/ac…6332 chars truncated… 2026-05-04: `php artisan test --compact` passed after the admin discount API changes: 218 tests, 1200 assertions.

 ## Decisions

@@ -371,7 +378,7 @@ ## Decisions
 - 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 REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are read-only products/customers/orders, collection CRUD, and order fulfillment/refund mutations.
+- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are read-only products/customers/orders, collection CRUD, discount CRUD, and order fulfillment/refund mutations.
 - Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; the dedicated analytics page now reads analytics event and daily aggregate tables.
 - Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.
 - Admin/store Livewire route middleware is registered with Livewire persistent middleware so component POST requests keep `current_store` binding and store-role checks after the initial full-page route load.
@@ -402,10 +409,10 @@ ## Open Issues

 - Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
 - `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.
-- Non-order admin REST write endpoints beyond collections and the larger admin REST surfaces for discounts, settings, themes, content, search, analytics, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for settings, themes, content, search, analytics, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
 - 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.

 ## Completion Summary

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, 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/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, 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/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.


Output:
Chunk ID: 47da14
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4083
Output:
diff --git a/routes/api.php b/routes/api.php
index a197d944..a0167e4e 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -2,6 +2,7 @@

 use App\Http\Controllers\Api\Admin\V1\CollectionController as AdminCollectionController;
 use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
+use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
 use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
 use App\Http\Controllers\Api\Admin\V1\OrderFulfillmentController as AdminOrderFulfillmentController;
 use App\Http\Controllers\Api\Admin\V1\OrderRefundController as AdminOrderRefundController;
@@ -73,6 +74,16 @@
             Route::delete('collections/{collection}', [AdminCollectionController::class, 'destroy'])->name('collections.destroy');
         });

+        Route::middleware('admin.api:read-discounts')->group(function (): void {
+            Route::get('discounts', [AdminDiscountController::class, 'index'])->name('discounts.index');
+        });
+
+        Route::middleware('admin.api:write-discounts')->group(function (): void {
+            Route::post('discounts', [AdminDiscountController::class, 'store'])->name('discounts.store');
+            Route::put('discounts/{discount}', [AdminDiscountController::class, 'update'])->name('discounts.update');
+            Route::delete('discounts/{discount}', [AdminDiscountController::class, 'destroy'])->name('discounts.destroy');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {
             Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
             Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
diff --git a/specs/progress.md b/specs/progress.md
index f684edc7..d247aedd 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -27,7 +27,7 @@ ## 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 including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, and webhook_deliveries. |
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/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}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. Write endpoints for remaining non-order admin REST surfaces are still missing. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/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}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. Write endpoints for remaining admin REST surfaces are still missing. |
 | Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, nested navigation menu editor with drag sorting and parent selection, theme grid, theme settings editor with iframe preview, theme file list/editor, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. |
 | Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus with one-level dropdown children, 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, customer password reset forms, 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, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `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-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
@@ -338,6 +338,13 @@ ## Verification Evidence
 - 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin collection API changes: 28 tests, 218 assertions.
 - 2026-05-04: `php artisan route:list --path=api/admin/v1/stores --except-vendor` confirmed 12 admin API routes, including collection list/create/update/delete.
 - 2026-05-04: `php artisan test --compact` passed after the admin collection API changes: 215 tests, 1175 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 validation, date validation, prohibited/unique update-field validation, JSON API testing, and Pest 4 docs before the admin discount API changes.
+- 2026-05-04: `php artisan make:controller Api/Admin/V1/DiscountController --no-interaction`, `php artisan make:resource Admin/V1/DiscountResource --no-interaction`, and `php artisan make:test Api/AdminDiscountApiTest --pest --no-interaction` created the discount API controller/resource and tests.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin discount API changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api/AdminDiscountApiTest.php` passed after the admin discount API changes: 3 tests, 25 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Api` passed after the admin discount API changes: 31 tests, 243 assertions.
+- 2026-05-04: `php artisan route:list --path=api/admin/v1/stores --except-vendor` confirmed 16 admin API routes, including discount list/create/update/delete.
+- 2026-05-04: `php artisan test --compact` passed after the admin discount API changes: 218 tests, 1200 assertions.

 ## Decisions

@@ -371,7 +378,7 @@ ## Decisions
 - 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 REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are read-only products/customers/orders, collection CRUD, and order fulfillment/refund mutations.
+- Admin REST API routes use the `admin.api` middleware with session-authenticated store users or hashed `oauth_tokens` bearer tokens; the current implemented surfaces are read-only products/customers/orders, collection CRUD, discount CRUD, and order fulfillment/refund mutations.
 - Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; the dedicated analytics page now reads analytics event and daily aggregate tables.
 - Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.
 - Admin/store Livewire route middleware is registered with Livewire persistent middleware so component POST requests keep `current_store` binding and store-role checks after the initial full-page route load.
@@ -402,10 +409,10 @@ ## Open Issues

 - Customer password reset is implemented under `/account/forgot-password` and `/account/reset-password/{token}`; the exact spec root paths remain occupied by the existing Fortify starter/admin reset routes.
 - `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.
-- Non-order admin REST write endpoints beyond collections and the larger admin REST surfaces for discounts, settings, themes, content, search, analytics, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
+- Admin REST surfaces for settings, themes, content, search, analytics, and exports are still missing; outbound webhook subscription management is implemented through the admin UI only, as specified for the initial implementation.
 - Full automated browser suites from Spec 08 are still incomplete beyond the initial Pest browser smoke coverage for storefront, admin, and mobile rendering.
 - 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.

 ## Completion Summary

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, 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/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, 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/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/order API surfaces, deferred OAuth/app route stubs, initial automated browser smoke coverage, and outbound webhook delivery foundations are implemented, with known auth/token route compatibility, broader admin REST, full browser-suite, and database CHECK constraint gaps tracked above.


sed -n '260,520p' app/Http/Controllers/Api/Admin/V1/DiscountController.php


sed -n '260,520p' app/Http/Controllers/Api/Admin/V1/DiscountController.php

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


Output:
Chunk ID: 5d455e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 63
Output:
     */
    private function integerList(mixed $value): array
    {
        return collect(is_array($value) ? $value : [])
            ->map(fn (mixed $id): int => (int) $id)
            ->unique()
            ->values()
            ->all();
    }
}


The final read matches the intended scope: a store-scoped discount CRUD API, JSON resource, route bindings, and focused feature coverage. I’m staging those files and committing this slice now.


git add app/Http/Controllers/Api/Admin/V1/DiscountController.php app/Http/Resources/Admin/V1/DiscountResource.php routes/api.php specs/progress.md tests/Feature/Api/AdminDiscountApiTest.php


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


 .../Api/Admin/V1/DiscountController.php            | 269 +++++++++++++++++++++
 app/Http/Resources/Admin/V1/DiscountResource.php   |  34 +++
 routes/api.php                                     |  11 +
 specs/progress.md                                  |  15 +-
 tests/Feature/Api/AdminDiscountApiTest.php         | 149 ++++++++++++
 5 files changed, 474 insertions(+), 4 deletions(-)


Output:
Chunk ID: b9115b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 97
Output:
 .../Api/Admin/V1/DiscountController.php            | 269 +++++++++++++++++++++
 app/Http/Resources/Admin/V1/DiscountResource.php   |  34 +++
 routes/api.php                                     |  11 +
 specs/progress.md                                  |  15 +-
 tests/Feature/Api/AdminDiscountApiTest.php         | 149 ++++++++++++
 5 files changed, 474 insertions(+), 4 deletions(-)


A  app/Http/Controllers/Api/Admin/V1/DiscountController.php
A  app/Http/Resources/Admin/V1/DiscountResource.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminDiscountApiTest.php


Output:
Chunk ID: b228c1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 50
Output:
A  app/Http/Controllers/Api/Admin/V1/DiscountController.php
A  app/Http/Resources/Admin/V1/DiscountResource.php
M  routes/api.php
M  specs/progress.md
A  tests/Feature/Api/AdminDiscountApiTest.php


git commit -m "Add admin discount API endpoints"


git commit -m "Add admin discount API endpoints"

[2026-05-04-codex-5-5-goal f138cefa] Add admin discount API endpoints
 5 files changed, 474 insertions(+), 4 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Admin/V1/DiscountController.php
 create mode 100644 app/Http/Resources/Admin/V1/DiscountResource.php
 create mode 100644 tests/Feature/Api/AdminDiscountApiTest.php


Output:
Chunk ID: a15bcc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 83
Output:
[2026-05-04-codex-5-5-goal f138cefa] Add admin discount API endpoints
 5 files changed, 474 insertions(+), 4 deletions(-)
 create mode 100644 app/Http/Controllers/Api/Admin/V1/DiscountController.php
 create mode 100644 app/Http/Resources/Admin/V1/DiscountResource.php
 create mode 100644 tests/Feature/Api/AdminDiscountApiTest.php


Committed `f138cefa` for the discount API. I’m moving to the next tracked gap in `specs/progress.md`; the smallest useful next slice is another admin REST surface, likely content/pages.


rg -n "pages|content|Admin REST|API" specs/02-API-ROUTES.md specs/09-IMPLEMENTATION-ROADMAP.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md


/bin/zsh
-lc
sed -n '1,240p' specs/02-API-ROUTES.md

# API and Routing Specification

This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.

All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).

---

## 1. Web Routes (Blade/Livewire Pages)

### 1.1 Admin Auth Routes

No middleware beyond the default `web` group. These routes handle admin login and logout.

| Method | Path | Description | Component / Action |
|--------|------|-------------|--------------------|
| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
| POST | `/admin/login` | Process admin login | Handled by Livewire action |
| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |

Rate limit: `login` tier (5 per minute per IP).

---

### 1.2 Admin Routes

**Prefix:** `/admin`
**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`

All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |

#### Products

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |

#### Inventory

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |

#### Collections

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |

#### Orders

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |

#### Customers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |

#### Discounts

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |

#### Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |

#### Themes and Online Store

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |

#### Pages

| Method | Path | Description | Component |
|--------|------|--…167 chars truncated… |
| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |

#### Navigation

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |

#### Search Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |

---

### 1.3 Storefront Routes

**Middleware:** `web`, `store.resolve`

These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |

---

## 2. Storefront REST API

**Prefix:** `/api/storefront/v1`
**Middleware:** `store.resolve`, `throttle:api.storefront`
**Content-Type:** `application/json`

These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).

---

### 2.1 Cart Endpoints

#### POST /api/storefront/v1/carts

Create a new cart.

- **Authentication:** None (guest) or session-based (logged-in customer)
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "currency": "EUR"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |

**Success response:** `201 Created`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 1,
  "status": "active",
  "lines": [],
  "totals": {
    "subtotal": 0,


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


Output:
Chunk ID: ad3d07
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2542
Output:
# API and Routing Specification

This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.

All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).

---

## 1. Web Routes (Blade/Livewire Pages)

### 1.1 Admin Auth Routes

No middleware beyond the default `web` group. These routes handle admin login and logout.

| Method | Path | Description | Component / Action |
|--------|------|-------------|--------------------|
| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
| POST | `/admin/login` | Process admin login | Handled by Livewire action |
| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |

Rate limit: `login` tier (5 per minute per IP).

---

### 1.2 Admin Routes

**Prefix:** `/admin`
**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`

All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |

#### Products

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |

#### Inventory

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |

#### Collections

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |

#### Orders

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |

#### Customers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |

#### Discounts

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |

#### Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |

#### Themes and Online Store

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |

#### Pages

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |

#### Navigation

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |

#### Search Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |

---

### 1.3 Storefront Routes

**Middleware:** `web`, `store.resolve`

These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |

---

## 2. Storefront REST API

**Prefix:** `/api/storefront/v1`
**Middleware:** `store.resolve`, `throttle:api.storefront`
**Content-Type:** `application/json`

These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).

---

### 2.1 Cart Endpoints

#### POST /api/storefront/v1/carts

Create a new cart.

- **Authentication:** None (guest) or session-based (logged-in customer)
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "currency": "EUR"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |

**Success response:** `201 Created`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 1,
  "status": "active",
  "lines": [],
  "totals": {
    "subtotal": 0,


/bin/zsh
-lc
sed -n '1,220p' specs/03-ADMIN-UI.md

# 03 - Admin Panel UI/UX Specification

This document defines every view, component, and interaction for the admin panel. The admin is entirely server-rendered using Laravel 12 + Livewire v4 + Flux UI Free components + Tailwind CSS v4. There is no SPA or client-side router.

**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip

Anything not covered by Flux UI Free uses plain Tailwind CSS v4 markup with Livewire directives.

---

## 1. Admin Layout Shell

The admin layout is a persistent shell that wraps every admin page. It consists of a left sidebar, a top bar, and a main content area.

### 1.1 Route Prefix

All admin routes live under `/admin` and are protected by authentication middleware plus store-scoping middleware. The store is resolved from the session or a store-selector in the top bar.

### 1.2 Sidebar Navigation

**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`

**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `collapsed` | `bool` | Whether the sidebar is collapsed (mobile state) |
| `currentRoute` | `string` | The current route name for active highlighting |

| Action | Behavior |
|--------|----------|
| `toggle()` | Toggles the `collapsed` state |

#### Wireframe

```
+-------------------------------+
|  [BRAND LOGO]  Platform Name  |
|-------------------------------|
|  [separator]                  |
|                               |
|  [chart-bar]    Dashboard     |
|                               |
|  PRODUCTS                     |
|  [cube]         Products      |
|  [rect-stack]   Collections   |
|  [archive-box]  Inventory     |
|                               |
|  [separator]                  |
|  ORDERS                       |
|  [shopping-bag] Orders        |
|                               |
|  [separator]                  |
|  CUSTOMERS                    |
|  [users]        Customers     |
|                               |
|  [separator]                  |
|  DISCOUNTS                    |
|  [tag]          Discounts     |
|                               |
|  [separator]                  |
|  CONTENT                      |
|  [doc-text]     Pages         |
|  [bars-3]       Navigation    |
|  [paint-brush]  Themes        |
|                               |
|  [separator]                  |
|  [chart-pie]    Analytics     |
|  [cog-6-tooth]  Settings      |
|  [squares-2x2]  Apps          |
|  [code-bracket] Developers    |
+-------------------------------+
```

#### Navigation Structure

```
Dashboard                        -> admin.dashboard
Products (group label)
  Products                       -> admin.products.index
  Collections                    -> admin.collections.index
  Inventory                      -> admin.inventory.index
Orders
  Orders                         -> admin.orders.index
Customers
  Customers                      -> admin.customers.index
Discounts
  Discounts                      -> admin.discounts.index
Content (group label)
  Pages                          -> admin.pages.index
  Navigation                     -> admin.navigation.index
  Themes                         -> admin.themes.index
Analytics                        -> admin.analytics.index
Settings                         -> admin.settings.index
Apps                             -> admin.apps.index
Developers                       -> admin.developers.index
```

Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Platform logo and name | `brand` |
| Navigation icons | `icon` |
| Group dividers | `separator` |

#### Responsive Behavior

- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.

### 1.3 Top Bar

**Livewire component:** `App\Livewire\Admin\Layout\TopBar`

**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `currentStoreName` | `string` | Name of the active store |
| `stores` | `Collection` | List of stores the user has access to |
| `unreadNotificationCount` | `int` | Count of unread notifications |

| Action | Behavior |
|--------|----------|
| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |

#### Wireframe

```
+--------------------------------------------------------------------+
| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
+--------------------------------------------------------------------+
  * hamburger visible on mobile only

  Store Name dropdown:
    - Store A
    - Store B
    - Store C

  Avatar dropdown:
    - Settings (links to /admin/settings)
    - [separator]
    - Log out (POST /logout with CSRF)
```

#### Interaction Flows

- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Store selector | `dropdown` |
| Hamburger (mobile) | `button` (icon: bars-3) |
| Notification bell | `button` (icon: bell, variant: ghost) |
| Notification count | `badge` |
| User profile trigger | `profile` |
| Profile menu | `dropdown` |
| Menu divider | `separator` |

### 1.4 Main Content Area

The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.

**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".

**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.

### 1.5 Toast Notifications

A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.

| Property | Value |
|----------|-------|
| Event name | `toast` |
| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
| Position | Top-right corner |
| Auto-dismiss | 5 seconds |
| Stacking | Supports multiple toasts |
| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |

Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").

### 1.6 Dark Mode

The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.

Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.

---

## 2. Dashboard

**Route:** `GET /admin`

**Livewire component:** `App\Livewire\Admin\Dashboard`

**Blade view:** `resources/views/livewire/admin/dashboard.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Active date range preset: `today`, `last_7_days`, `last_30_days`, `custom` |
| `customStartDate` | `?string` | Custom range start (Y-m-d) |
| `customEndDate` | `?string` | Custom range end (Y-m-d) |
| `totalSales` | `int` | Total sales amount in minor units |
| `ordersCount` | `int` | Number of orders in range |
| `averageOrderValue` | `int` | Average order value in minor units |
| `visitorsCount` | `int` | Unique visitors in range |


Output:
Chunk ID: 191dc3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2195
Output:
# 03 - Admin Panel UI/UX Specification

This document defines every view, component, and interaction for the admin panel. The admin is entirely server-rendered using Laravel 12 + Livewire v4 + Flux UI Free components + Tailwind CSS v4. There is no SPA or client-side router.

**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip

Anything not covered by Flux UI Free uses plain Tailwind CSS v4 markup with Livewire directives.

---

## 1. Admin Layout Shell

The admin layout is a persistent shell that wraps every admin page. It consists of a left sidebar, a top bar, and a main content area.

### 1.1 Route Prefix

All admin routes live under `/admin` and are protected by authentication middleware plus store-scoping middleware. The store is resolved from the session or a store-selector in the top bar.

### 1.2 Sidebar Navigation

**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`

**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `collapsed` | `bool` | Whether the sidebar is collapsed (mobile state) |
| `currentRoute` | `string` | The current route name for active highlighting |

| Action | Behavior |
|--------|----------|
| `toggle()` | Toggles the `collapsed` state |

#### Wireframe

```
+-------------------------------+
|  [BRAND LOGO]  Platform Name  |
|-------------------------------|
|  [separator]                  |
|                               |
|  [chart-bar]    Dashboard     |
|                               |
|  PRODUCTS                     |
|  [cube]         Products      |
|  [rect-stack]   Collections   |
|  [archive-box]  Inventory     |
|                               |
|  [separator]                  |
|  ORDERS                       |
|  [shopping-bag] Orders        |
|                               |
|  [separator]                  |
|  CUSTOMERS                    |
|  [users]        Customers     |
|                               |
|  [separator]                  |
|  DISCOUNTS                    |
|  [tag]          Discounts     |
|                               |
|  [separator]                  |
|  CONTENT                      |
|  [doc-text]     Pages         |
|  [bars-3]       Navigation    |
|  [paint-brush]  Themes        |
|                               |
|  [separator]                  |
|  [chart-pie]    Analytics     |
|  [cog-6-tooth]  Settings      |
|  [squares-2x2]  Apps          |
|  [code-bracket] Developers    |
+-------------------------------+
```

#### Navigation Structure

```
Dashboard                        -> admin.dashboard
Products (group label)
  Products                       -> admin.products.index
  Collections                    -> admin.collections.index
  Inventory                      -> admin.inventory.index
Orders
  Orders                         -> admin.orders.index
Customers
  Customers                      -> admin.customers.index
Discounts
  Discounts                      -> admin.discounts.index
Content (group label)
  Pages                          -> admin.pages.index
  Navigation                     -> admin.navigation.index
  Themes                         -> admin.themes.index
Analytics                        -> admin.analytics.index
Settings                         -> admin.settings.index
Apps                             -> admin.apps.index
Developers                       -> admin.developers.index
```

Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Platform logo and name | `brand` |
| Navigation icons | `icon` |
| Group dividers | `separator` |

#### Responsive Behavior

- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.

### 1.3 Top Bar

**Livewire component:** `App\Livewire\Admin\Layout\TopBar`

**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `currentStoreName` | `string` | Name of the active store |
| `stores` | `Collection` | List of stores the user has access to |
| `unreadNotificationCount` | `int` | Count of unread notifications |

| Action | Behavior |
|--------|----------|
| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |

#### Wireframe

```
+--------------------------------------------------------------------+
| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
+--------------------------------------------------------------------+
  * hamburger visible on mobile only

  Store Name dropdown:
    - Store A
    - Store B
    - Store C

  Avatar dropdown:
    - Settings (links to /admin/settings)
    - [separator]
    - Log out (POST /logout with CSRF)
```

#### Interaction Flows

- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Store selector | `dropdown` |
| Hamburger (mobile) | `button` (icon: bars-3) |
| Notification bell | `button` (icon: bell, variant: ghost) |
| Notification count | `badge` |
| User profile trigger | `profile` |
| Profile menu | `dropdown` |
| Menu divider | `separator` |

### 1.4 Main Content Area

The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.

**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".

**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.

### 1.5 Toast Notifications

A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.

| Property | Value |
|----------|-------|
| Event name | `toast` |
| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
| Position | Top-right corner |
| Auto-dismiss | 5 seconds |
| Stacking | Supports multiple toasts |
| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |

Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").

### 1.6 Dark Mode

The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.

Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.

---

## 2. Dashboard

**Route:** `GET /admin`

**Livewire component:** `App\Livewire\Admin\Dashboard`

**Blade view:** `resources/views/livewire/admin/dashboard.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Active date range preset: `today`, `last_7_days`, `last_30_days`, `custom` |
| `customStartDate` | `?string` | Custom range start (Y-m-d) |
| `customEndDate` | `?string` | Custom range end (Y-m-d) |
| `totalSales` | `int` | Total sales amount in minor units |
| `ordersCount` | `int` | Number of orders in range |
| `averageOrderValue` | `int` | Average order value in minor units |
| `visitorsCount` | `int` | Unique visitors in range |


rg -n "pages|content|Admin REST|API" specs/02-API-ROUTES.md specs/09-IMPLEMENTATION-ROADMAP.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md

rg -n 'pages|content|Admin REST|API' specs/02-API-ROUTES.md specs/09-IMPLEMENTATION-ROADMAP.md specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md

Question: pages|content|Admin REST|API

specs/02-API-ROUTES.md:1:# API and Routing Specification
specs/02-API-ROUTES.md:3:This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.
specs/02-API-ROUTES.md:34:All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).
specs/02-API-ROUTES.md:107:| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
specs/02-API-ROUTES.md:108:| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
specs/02-API-ROUTES.md:109:| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |
specs/02-API-ROUTES.md:128:| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |
specs/02-API-ROUTES.md:130:> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.
specs/02-API-ROUTES.md:160:| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |
specs/02-API-ROUTES.md:197:## 2. Storefront REST API
specs/02-API-ROUTES.md:422:    participant API as Storefront API
specs/02-API-ROUTES.md:425:    C->>API: POST /checkouts (cart_id, email)
specs/02-API-ROUTES.md:426:    API-->>C: 201 Checkout (status: started)
specs/02-API-ROUTES.md:428:    C->>API: PUT /checkouts/{id}/address
specs/02-API-ROUTES.md:429:    API-->>C: 200 Checkout (status: addressed, available_shipping_methods)
specs/02-API-ROUTES.md:431:    C->>API: PUT /checkouts/{id}/shipping-method
specs/02-API-ROUTES.md:432:    API-->>C: 200 Checkout (status: shipping_selected, totals with tax)
specs/02-API-ROUTES.md:435:        C->>API: POST /checkouts/{id}/apply-discount
specs/02-API-ROUTES.md:436:        API-->>C: 200 Checkout (totals recalculated)
specs/02-API-ROUTES.md:439:    C->>API: POST /checkouts/{id}/pay (payment_method + details)
specs/02-API-ROUTES.md:440:    API->>API: Reserve inventory
specs/02-API-ROUTES.md:441:    API->>MockPSP: charge(checkout, paymentMethodData)
specs/02-API-ROUTES.md:444:        MockPSP-->>API: PaymentResult (success)
specs/02-API-ROUTES.md:445:        API->>API: Create order (paid), commit inventory
specs/02-API-ROUTES.md:446:        API-->>C: 200 (order confirmation data)
specs/02-API-ROUTES.md:448:        MockPSP-->>API: PaymentResult (failed)
specs/02-API-ROUTES.md:449:        API->>API: Release reserved inventory
specs/02-API-ROUTES.md:450:        API-->>C: 422 (error: card_declined / insufficient_funds)
specs/02-API-ROUTES.md:452:        MockPSP-->>API: PaymentResult (pending)
specs/02-API-ROUTES.md:453:        API->>API: Create order (pending), inventory stays reserved
specs/02-API-ROUTES.md:454:        API-->>C: 200 (order confirmation + bank transfer instructions)
specs/02-API-ROUTES.md:850:Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.
specs/02-API-ROUTES.md:1206:## 3. Admin REST API
specs/02-API-ROUTES.md:1214:All endpoints require a valid Sanctum API token with appropriate scopes. Responses follow Laravel's API Resource conventions.
specs/02-API-ROUTES.md:1703:  "content_type": "image/jpeg",
specs/02-API-ROUTES.md:1711:| `content_type` | string | Yes | One of: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `video/mp4`. |
specs/02-API-ROUTES.md:1732:- `422` - Validation errors (invalid content type, file too large)
specs/02-API-ROUTES.md:2604:| GET | `/api/admin/v1/stores/{storeId}/pages` | `read-content` | List content pages | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2605:| POST | `/api/admin/v1/stores/{storeId}/pages` | `write-content` | Create a content page | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2606:| PUT | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Update a content page | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2607:| DELETE | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Delete a content page | `api.admin` (60/min) |
specs/02-API-ROUTES.md:2609:#### GET /api/admin/v1/stores/{storeId}/pages
specs/02-API-ROUTES.md:2611:List content pages.
specs/02-API-ROUTES.md:2648:#### POST /api/admin/v1/stores/{storeId}/pages
specs/02-API-ROUTES.md:2650:Create a content page.
specs/02-API-ROUTES.md:2678:#### PUT /api/admin/v1/stores/{storeId}/pages/{pageId}
specs/02-API-ROUTES.md:2680:Update a content page.
specs/02-API-ROUTES.md:2693:#### DELETE /api/admin/v1/stores/{storeId}/pages/{pageId}
specs/02-API-ROUTES.md:2695:Delete a content page.
specs/02-API-ROUTES.md:2697:**Authentication:** Bearer …6647 chars truncated…ventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:358:4. `create_pages_table` - FK: `store_id`
specs/09-IMPLEMENTATION-ROADMAP.md:401:| `pages/` | `home.blade.php`, `collection.blade.php`, `product.blade.php`, `cart.blade.php`, `search.blade.php`, `page.blade.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:409:Base layout (`app.blade.php`): header with nav, announcement bar, main content, footer, cart drawer. All with dark mode support via `dark:` prefix.
specs/09-IMPLEMENTATION-ROADMAP.md:507:All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.
specs/09-IMPLEMENTATION-ROADMAP.md:672:- No external API calls, no webhooks, no redirects
specs/09-IMPLEMENTATION-ROADMAP.md:714:| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |
specs/09-IMPLEMENTATION-ROADMAP.md:845:| `Admin\Pages\Index` | Content pages list |
specs/09-IMPLEMENTATION-ROADMAP.md:852:| `Admin\Developers\Index` | API token management, webhook subscriptions |
specs/09-IMPLEMENTATION-ROADMAP.md:864:3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table
specs/09-IMPLEMENTATION-ROADMAP.md:957:1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
specs/09-IMPLEMENTATION-ROADMAP.md:958:2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
specs/09-IMPLEMENTATION-ROADMAP.md:960:4. Error pages: styled 404 and 503 pages matching the storefront theme
specs/09-IMPLEMENTATION-ROADMAP.md:1194:| authenticates API request with valid token | GET /api/admin/v1/stores/{storeId}/products with Bearer token | 200 response | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1195:| rejects API request with invalid token | GET /api/admin/v1/stores/{storeId}/products with fake token | 401 response | validation |
specs/09-IMPLEMENTATION-ROADMAP.md:1197:| revokes a token | create token, then delete it; make API request with revoked token | 401 response | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1301:| creates a cart via API | POST /api/storefront/v1/carts | 201 response, response contains cart data | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1302:| retrieves a cart via API | create cart, then GET /api/storefront/v1/carts/{id} | 200 response, cart data with lines and totals | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1303:| adds a line via API | POST /api/storefront/v1/carts/{id}/lines with variant_id and quantity | 200 response, line added to cart | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1304:| updates line quantity via API | PUT /api/storefront/v1/carts/{id}/lines/{lineId} with quantity | 200 response, quantity updated | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1305:| removes a line via API | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1669:| creates a product via API | POST /api/admin/v1/stores/{storeId}/products with product data | 201 response, product in response | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1670:| updates a product via API | PUT /api/admin/v1/stores/{storeId}/products/{id} with updated data | 200 response, product updated | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1671:| deletes a draft product via API | DELETE /api/admin/v1/stores/{storeId}/products/{id} | 200 response | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1685:| creates a fulfillment via API | POST /api/admin/v1/stores/{storeId}/orders/{id}/fulfillments | 201 response, fulfillment created | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1686:| creates a refund via API | POST /api/admin/v1/stores/{storeId}/orders/{id}/refunds | 201 response, refund created | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1772:| `Admin/Developers/Index` | API token and webhook management |
specs/09-IMPLEMENTATION-ROADMAP.md:1888:| `Auth/SanctumTokenTest` | API token feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1895:| `Cart/CartApiTest` | Cart API feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1921:| `Api/StorefrontCartApiTest` | Storefront cart API feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1922:| `Api/StorefrontCheckoutApiTest` | Storefront checkout API feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1923:| `Api/AdminProductApiTest` | Admin product API feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1924:| `Api/AdminOrderApiTest` | Admin order API feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1952:| `routes/api.php` | Storefront API (prefix `/api/storefront/v1`), Admin API (prefix `/api/admin/v1`), Webhook receiver routes |
specs/09-IMPLEMENTATION-ROADMAP.md:1977:| `RefundService` | Refund creation, provider API, inventory restock |