command: sed -n '1010,1215p' specs/02-API-ROUTES.md

aggregated_output:
**Error responses:**
- `401` - Invalid or missing token
- `404` - Order not found

---

### 2.5 Search Endpoints

#### GET /api/storefront/v1/search

Search products within the current store.

- **Authentication:** None
- **Rate limit:** `search` (30/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | Yes | Search query. Min: 1, max: 200 characters. |
| `filters` | json | No | URL-encoded JSON object with filter facets. |
| `sort` | string | No | One of: `relevance`, `price_asc`, `price_desc`, `newest`, `best_selling`. Default: `relevance`. |
| `page` | integer | No | Page number. Default: 1. |
| `per_page` | integer | No | Results per page. Min: 1, max: 50. Default: 24. |

**Filters JSON schema:**

```json
{
  "collection_id": 5,
  "price_min": 1000,
  "price_max": 5000,
  "in_stock": true,
  "tags": ["organic", "cotton"],
  "vendor": "Acme Apparel"
}
```

**Success response:** `200 OK`

```json
{
  "query": "t-shirt",
  "results": [
    {
      "id": 10,
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "vendor": "Acme Apparel",
      "product_type": "Apparel",
      "price_amount": 2500,
      "compare_at_amount": 3500,
      "currency": "EUR",
      "image_url": "https://cdn.example.com/products/tshirt.jpg",
      "in_stock": true,
      "tags": ["organic", "cotton"]
    }
  ],
  "facets": {
    "vendors": [
      { "value": "Acme Apparel", "count": 12 }
    ],
    "tags": [
      { "value": "organic", "count": 8 },
      { "value": "cotton", "count": 15 }
    ],
    "price_range": {
      "min": 1500,
      "max": 8500
    }
  },
  "pagination": {
    "current_page": 1,
    "per_page": 24,
    "total": 42,
    "last_page": 2
  }
}
```

**Error responses:**
- `400` - Query too long (max 200 characters) or malformed filters
- `422` - Validation error

---

#### GET /api/storefront/v1/search/suggest

Autocomplete suggestions as the user types.

- **Authentication:** None
- **Rate limit:** `search` (30/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | Yes | Search prefix. Min: 1, max: 100 characters. |
| `limit` | integer | No | Max suggestions. Default: 5, max: 10. |

**Success response:** `200 OK`

```json
{
  "query": "t-sh",
  "suggestions": [
    {
      "type": "product",
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "image_url": "https://cdn.example.com/products/tshirt-thumb.jpg",
      "price_amount": 2500,
      "currency": "EUR"
    },
    {
      "type": "collection",
      "title": "T-Shirts",
      "handle": "t-shirts",
      "image_url": null
    }
  ]
}
```

**Error responses:**
- `400` - Query too long
- `422` - Validation error

---

### 2.6 Analytics Endpoints

#### POST /api/storefront/v1/analytics/events

Batch submit storefront analytics events.

- **Authentication:** None
- **Rate limit:** `analytics` (60/min per IP)

**Request body:**

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "sess_abc123",
      "client_event_id": "evt_001",
      "properties": {
        "url": "/products/classic-t-shirt",
        "referrer": "https://google.com",
        "user_agent": "Mozilla/5.0..."
      },
      "occurred_at": "2026-02-11T10:00:00Z"
    },
    {
      "type": "add_to_cart",
      "session_id": "sess_abc123",
      "client_event_id": "evt_002",
      "properties": {
        "product_id": 10,
        "variant_id": 101,
        "quantity": 1,
        "price_amount": 2500,
        "currency": "EUR"
      },
      "occurred_at": "2026-02-11T10:01:00Z"
    }
  ]
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `events` | array | Yes | Min: 1, max: 50 events per batch. |
| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
| `events[].session_id` | string | Yes | Max: 100 characters. |
| `events[].client_event_id` | string | Yes | Max: 100 characters. If a `client_event_id` has already been recorded for the current store, the duplicate event is silently dropped (unique on `(store_id, client_event_id)`). |
| `events[].properties` | object | No | Freeform JSON. Max depth: 3 levels. |
| `events[].occurred_at` | string (ISO 8601) | Yes | Must not be more than 1 hour in the past or in the future. |

**Success response:** `202 Accepted`

```json
{
  "accepted": 2,
  "rejected": 0
}
```

**Error responses:**
- `422` - Validation errors (invalid event types, too many events, etc.)

---

## 3. Admin REST API

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

These endpoints provide programmatic access for integrations, CLI tools, and third-party apps. The admin web UI uses Livewire actions directly and does not call these endpoints.

All endpoints require a valid Sanctum API token with appropriate scopes. Responses follow Laravel's API Resource conventions.


command: sed -n '850,940p' specs/09-IMPLEMENTATION-ROADMAP.md

type: read

aggregated_output:
| `Admin\Apps\Index` | App directory |
| `Admin\Apps\Show` | Installed app detail |
| `Admin\Developers\Index` | API token management, webhook subscriptions |

---

### Phase 8: Search

**Priority:** LOW - enhances product discovery.

#### Step 8.1: Migrations

1. `create_search_settings_table` - PK: `store_id`
2. `create_search_queries_table` - FK: `store_id`
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

#### Step 8.2: Search Service

**File to create:** `App\Services\SearchService`

| Method | Description |
|--------|-------------|
| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
| `syncProduct(Product $product): void` | Upsert into FTS5 index |
| `removeProduct(int $productId): void` | Delete from FTS5 index |

**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.

#### Step 8.3: Search UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |

---

### Phase 9: Analytics

**Priority:** LOW - reporting and insights.

#### Step 9.1: Migrations

1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

**File to create:** `App\Services\AnalyticsService`

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

**File to create:** `App\Jobs\AggregateAnalytics`

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

**File to create:** `App\Services\WebhookService`

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |

status: completed


command: rg --files app database routes resources/views tests | sort

path: app

aggregated_output:
app/Actions/Fortify/CreateNewUser.php
app/Actions/Fortify/ResetUserPassword.php
app/Auth/CustomerUserProvider.php
app/Concerns/PasswordValidationRules.php
app/Concerns/ProfileValidationRules.php
app/Contracts/PaymentProvider.php
app/Contracts/TaxProvider.php
app/Enums/CartStatus.php
app/Enums/CheckoutStatus.php
app/Enums/CollectionStatus.php
app/Enums/CollectionType.php
app/Enums/DiscountStatus.php
app/Enums/DiscountType.php
app/Enums/DiscountValueType.php
app/Enums/FinancialStatus.php
app/Enums/FulfillmentShipmentStatus.php
app/Enums/FulfillmentStatus.php
app/Enums/InventoryPolicy.php
app/Enums/MediaStatus.php
app/Enums/MediaType.php
app/Enums/NavigationItemType.php
app/Enums/OrderStatus.php
app/Enums/PageStatus.php
app/Enums/PaymentMethod.php
app/Enums/PaymentStatus.php
app/Enums/ProductStatus.php
app/Enums/RefundStatus.php
app/Enums/ShippingRateType.php
app/Enums/StoreDomainType.php
app/Enums/StoreStatus.php
app/Enums/StoreUserRole.php
app/Enums/TaxMode.php
app/Enums/ThemeStatus.php
app/Enums/VariantStatus.php
app/Events/FulfillmentCreated.php
app/Events/FulfillmentDelivered.php
app/Events/FulfillmentShipped.php
app/Events/OrderCancelled.php
app/Events/OrderCreated.php
app/Events/OrderPaid.php
app/Events/OrderRefunded.php
app/Events/ProductStatusChanged.php
app/Exceptions/CartVersionMismatchException.php
app/Exceptions/InsufficientInventoryException.php
app/Exceptions/InvalidCartOperationException.php
app/Exceptions/InvalidCheckoutTransitionException.php
app/Exceptions/InvalidDiscountException.php
app/Exceptions/InvalidFulfillmentOperationException.php
app/Exceptions/InvalidOrderOperationException.php
app/Exceptions/InvalidProductTransitionException.php
app/Exceptions/InvalidRefundOperationException.php
app/Exceptions/PaymentFailedException.php
app/Exceptions/UnserviceableShippingAddressException.php
app/Http/Controllers/Api/Admin/V1/OrderController.php
app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php
app/Http/Controllers/Api/Storefront/V1/CartController.php
app/Http/Controllers/Api/Storefront/V1/CartLineController.php
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
app/Http/Controllers/Api/Storefront/V1/OrderController.php
app/Http/Controllers/Controller.php
app/Http/Middleware/CheckStoreRole.php
app/Http/Middleware/EnsureUserEmailIsVerified.php
app/Http/Middleware/ResolveStore.php
app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php
app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php
app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/SelectCheckoutPaymentRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutShippingRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php
app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php
app/Http/Resources/Admin/V1/FulfillmentResource.php
app/Http/Resources/Admin/V1/OrderResource.php
app/Http/Resources/Admin/V1/RefundResource.php
app/Http/Resources/Storefront/V1/CartLineResource.php
app/Http/Resources/Storefront/V1/CartResource.php
app/Http/Resources/Storefront/V1/CheckoutResource.php
app/Http/Resources/Storefront/V1/OrderResource.php
app/Http/Resources/Storefront/V1/ShippingRateResource.php
app/Jobs/CancelUnpaidBankTransferOrders.php
app/Jobs/CleanupAbandonedCarts.php
app/Jobs/ExpireAbandonedCheckouts.php
app/Jobs/ProcessMediaUpload.php
app/Livewire/Actions/Logout.php
app/Livewire/Admin/Auth/Login.php
app/Livewire/Admin/Collections/Form.php
app/Livewire/Admin/Collections/Index.php
app/Livewire/Admin/Customers/Index.php
app/Livewire/Admin/Customers/Show.php
app/Livewire/Admin/Dashboard.php
app/Livewire/Admin/Discounts/Form.php
app/Livewire/Admin/Discounts/Index.php
app/Livewire/Admin/Inventory/Index.php
app/Livewire/Admin/Navigation/Index.php
app/Livewire/Admin/Orders/Index.php
app/Livewire/Admin/Orders/Show.php
app/Livewire/Admin/Pages/Form.php
app/Livewire/Admin/Pages/Index.php
app/Livewire/Admin/Products/Form.php
app/Livewire/Admin/Products/Index.php
app/Livewire/Admin/Settings/Index.php
app/Livewire/Admin/Settings/Shipping.php
app/Livewire/Admin/Settings/Taxes.php
app/Livewire/Admin/Themes/Editor.php
app/Livewire/Admin/Themes/Index.php
app/Livewire/Settings/Appearance.php
app/Livewire/Settings/DeleteUserForm.php
app/Livewire/Settings/Password.php
app/Livewire/Settings/Profile.php
app/Livewire/Settings/TwoFactor.php
app/Livewire/Settings/TwoFactor/RecoveryCodes.php
app/Livewire/Storefront/Account/Auth/Login.php
app/Livewire/Storefront/Account/Auth/Register.php
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Li…9216 chars truncated…s/auth/simple.blade.php
resources/views/layouts/auth/split.blade.php
resources/views/layouts/storefront.blade.php
resources/views/livewire/admin/auth/login.blade.php
resources/views/livewire/admin/collections/form.blade.php
resources/views/livewire/admin/collections/index.blade.php
resources/views/livewire/admin/customers/index.blade.php
resources/views/livewire/admin/customers/show.blade.php
resources/views/livewire/admin/dashboard.blade.php
resources/views/livewire/admin/discounts/form.blade.php
resources/views/livewire/admin/discounts/index.blade.php
resources/views/livewire/admin/inventory/index.blade.php
resources/views/livewire/admin/navigation/index.blade.php
resources/views/livewire/admin/orders/index.blade.php
resources/views/livewire/admin/orders/show.blade.php
resources/views/livewire/admin/pages/form.blade.php
resources/views/livewire/admin/pages/index.blade.php
resources/views/livewire/admin/products/form.blade.php
resources/views/livewire/admin/products/index.blade.php
resources/views/livewire/admin/settings/index.blade.php
resources/views/livewire/admin/settings/shipping.blade.php
resources/views/livewire/admin/settings/taxes.blade.php
resources/views/livewire/admin/themes/editor.blade.php
resources/views/livewire/admin/themes/index.blade.php
resources/views/livewire/auth/confirm-password.blade.php
resources/views/livewire/auth/forgot-password.blade.php
resources/views/livewire/auth/login.blade.php
resources/views/livewire/auth/register.blade.php
resources/views/livewire/auth/reset-password.blade.php
resources/views/livewire/auth/two-factor-challenge.blade.php
resources/views/livewire/auth/verify-email.blade.php
resources/views/livewire/settings/appearance.blade.php
resources/views/livewire/settings/delete-user-form.blade.php
resources/views/livewire/settings/password.blade.php
resources/views/livewire/settings/profile.blade.php
resources/views/livewire/settings/two-factor.blade.php
resources/views/livewire/settings/two-factor/recovery-codes.blade.php
resources/views/livewire/storefront/account/auth/login.blade.php
resources/views/livewire/storefront/account/auth/register.blade.php
resources/views/livewire/storefront/account/orders/index.blade.php
resources/views/livewire/storefront/account/orders/show.blade.php
resources/views/livewire/storefront/cart-drawer.blade.php
resources/views/livewire/storefront/cart/show.blade.php
resources/views/livewire/storefront/checkout/confirmation.blade.php
resources/views/livewire/storefront/checkout/show.blade.php
resources/views/livewire/storefront/collections/index.blade.php
resources/views/livewire/storefront/collections/show.blade.php
resources/views/livewire/storefront/home.blade.php
resources/views/livewire/storefront/pages/show.blade.php
resources/views/livewire/storefront/products/show.blade.php
resources/views/livewire/storefront/search/index.blade.php
resources/views/partials/head.blade.php
resources/views/partials/settings-heading.blade.php
resources/views/storefront/account/dashboard.blade.php
resources/views/welcome.blade.php
routes/api.php
routes/console.php
routes/settings.php
routes/web.php
tests/Feature/Admin/ContentManagementTest.php
tests/Feature/Admin/CustomerManagementTest.php
tests/Feature/Admin/DashboardTest.php
tests/Feature/Admin/DiscountManagementTest.php
tests/Feature/Admin/OrderManagementTest.php
tests/Feature/Admin/SettingsManagementTest.php
tests/Feature/Api/AdminOrderApiTest.php
tests/Feature/Api/StorefrontCartApiTest.php
tests/Feature/Api/StorefrontCheckoutApiTest.php
tests/Feature/Api/StorefrontOrderApiTest.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Auth/EmailVerificationTest.php
tests/Feature/Auth/PasswordConfirmationTest.php
tests/Feature/Auth/PasswordResetTest.php
tests/Feature/Auth/RegistrationTest.php
tests/Feature/Auth/TwoFactorChallengeTest.php
tests/Feature/Cart/CartServiceTest.php
tests/Feature/Catalog/CatalogFoundationTest.php
tests/Feature/Catalog/CatalogUiTest.php
tests/Feature/Catalog/InventoryServiceTest.php
tests/Feature/Catalog/MediaProcessingTest.php
tests/Feature/Catalog/ProductServiceTest.php
tests/Feature/Catalog/VariantMatrixServiceTest.php
tests/Feature/Checkout/CheckoutServiceTest.php
tests/Feature/Checkout/PricingServicesTest.php
tests/Feature/DashboardTest.php
tests/Feature/ExampleTest.php
tests/Feature/Foundation/AuthorizationTest.php
tests/Feature/Foundation/CustomerAuthTest.php
tests/Feature/Foundation/TenancyTest.php
tests/Feature/Orders/BankTransferOrderTest.php
tests/Feature/Orders/FulfillmentServiceTest.php
tests/Feature/Orders/OrderServiceTest.php
tests/Feature/Orders/RefundServiceTest.php
tests/Feature/Payments/MockPaymentProviderTest.php
tests/Feature/Settings/PasswordUpdateTest.php
tests/Feature/Settings/ProfileUpdateTest.php
tests/Feature/Settings/TwoFactorAuthenticationTest.php
tests/Feature/Storefront/CartCheckoutUiTest.php
tests/Feature/Storefront/OrderViewsTest.php
tests/Feature/Storefront/ThemeDataTest.php
tests/Pest.php
tests/TestCase.php
tests/Unit/ExampleTest.php


output:
Chunk ID: f289e4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4804
Output:
app/Actions/Fortify/CreateNewUser.php
app/Actions/Fortify/ResetUserPassword.php
app/Auth/CustomerUserProvider.php
app/Concerns/PasswordValidationRules.php
app/Concerns/ProfileValidationRules.php
app/Contracts/PaymentProvider.php
app/Contracts/TaxProvider.php
app/Enums/CartStatus.php
app/Enums/CheckoutStatus.php
app/Enums/CollectionStatus.php
app/Enums/CollectionType.php
app/Enums/DiscountStatus.php
app/Enums/DiscountType.php
app/Enums/DiscountValueType.php
app/Enums/FinancialStatus.php
app/Enums/FulfillmentShipmentStatus.php
app/Enums/FulfillmentStatus.php
app/Enums/InventoryPolicy.php
app/Enums/MediaStatus.php
app/Enums/MediaType.php
app/Enums/NavigationItemType.php
app/Enums/OrderStatus.php
app/Enums/PageStatus.php
app/Enums/PaymentMethod.php
app/Enums/PaymentStatus.php
app/Enums/ProductStatus.php
app/Enums/RefundStatus.php
app/Enums/ShippingRateType.php
app/Enums/StoreDomainType.php
app/Enums/StoreStatus.php
app/Enums/StoreUserRole.php
app/Enums/TaxMode.php
app/Enums/ThemeStatus.php
app/Enums/VariantStatus.php
app/Events/FulfillmentCreated.php
app/Events/FulfillmentDelivered.php
app/Events/FulfillmentShipped.php
app/Events/OrderCancelled.php
app/Events/OrderCreated.php
app/Events/OrderPaid.php
app/Events/OrderRefunded.php
app/Events/ProductStatusChanged.php
app/Exceptions/CartVersionMismatchException.php
app/Exceptions/InsufficientInventoryException.php
app/Exceptions/InvalidCartOperationException.php
app/Exceptions/InvalidCheckoutTransitionException.php
app/Exceptions/InvalidDiscountException.php
app/Exceptions/InvalidFulfillmentOperationException.php
app/Exceptions/InvalidOrderOperationException.php
app/Exceptions/InvalidProductTransitionException.php
app/Exceptions/InvalidRefundOperationException.php
app/Exceptions/PaymentFailedException.php
app/Exceptions/UnserviceableShippingAddressException.php
app/Http/Controllers/Api/Admin/V1/OrderController.php
app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php
app/Http/Controllers/Api/Storefront/V1/CartController.php
app/Http/Controllers/Api/Storefront/V1/CartLineController.php
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php
app/Http/Controllers/Api/Storefront/V1/OrderController.php
app/Http/Controllers/Controller.php
app/Http/Middleware/CheckStoreRole.php
app/Http/Middleware/EnsureUserEmailIsVerified.php
app/Http/Middleware/ResolveStore.php
app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
app/Http/Requests/Api/Storefront/V1/ApplyCheckoutDiscountRequest.php
app/Http/Requests/Api/Storefront/V1/CompleteCheckoutPaymentRequest.php
app/Http/Requests/Api/Storefront/V1/DestroyCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/SelectCheckoutPaymentRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutAddressRequest.php
app/Http/Requests/Api/Storefront/V1/SetCheckoutShippingRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartLineRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php
app/Http/Requests/Api/Storefront/V1/StoreCheckoutRequest.php
app/Http/Requests/Api/Storefront/V1/UpdateCartLineRequest.php
app/Http/Resources/Admin/V1/FulfillmentResource.php
app/Http/Resources/Admin/V1/OrderResource.php
app/Http/Resources/Admin/V1/RefundResource.php
app/Http/Resources/Storefront/V1/CartLineResource.php
app/Http/Resources/Storefront/V1/CartResource.php
app/Http/Resources/Storefront/V1/CheckoutResource.php
app/Http/Resources/Storefront/V1/OrderResource.php
app/Http/Resources/Storefront/V1/ShippingRateResource.php
app/Jobs/CancelUnpaidBankTransferOrders.php
app/Jobs/CleanupAbandonedCarts.php
app/Jobs/ExpireAbandonedCheckouts.php
app/Jobs/ProcessMediaUpload.php
app/Livewire/Actions/Logout.php
app/Livewire/Admin/Auth/Login.php
app/Livewire/Admin/Collections/Form.php
app/Livewire/Admin/Collections/Index.php
app/Livewire/Admin/Customers/Index.php
app/Livewire/Admin/Customers/Show.php
app/Livewire/Admin/Dashboard.php
app/Livewire/Admin/Discounts/Form.php
app/Livewire/Admin/Discounts/Index.php
app/Livewire/Admin/Inventory/Index.php
app/Livewire/Admin/Navigation/Index.php
app/Livewire/Admin/Orders/Index.php
app/Livewire/Admin/Orders/Show.php
app/Livewire/Admin/Pages/Form.php
app/Livewire/Admin/Pages/Index.php
app/Livewire/Admin/Products/Form.php
app/Livewire/Admin/Products/Index.php
app/Livewire/Admin/Settings/Index.php
app/Livewire/Admin/Settings/Shipping.php
app/Livewire/Admin/Settings/Taxes.php
app/Livewire/Admin/Themes/Editor.php
app/Livewire/Admin/Themes/Index.php
app/Livewire/Settings/Appearance.php
app/Livewire/Settings/DeleteUserForm.php
app/Livewire/Settings/Password.php
app/Livewire/Settings/Profile.php
app/Livewire/Settings/TwoFactor.php
app/Livewire/Settings/TwoFactor/RecoveryCodes.php
app/Livewire/Storefront/Account/Auth/Login.php
app/Livewire/Storefront/Account/Auth/Register.php
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Livewire/Storefront/Cart/Show.php
app/Livewire/Storefront/CartDrawer.php
app/Livewire/Storefront/Checkout/Confirmation.php
app/Livewire/Storefront/Checkout/Show.php
app/Livewire/Storefront/Collections/Index.php
app/Livewire/Storefront/Collections/Show.php
app/Livewire/Storefront/Home.php
app/Livewire/Storefront/Pages/Show.php
app/Livewire/Storefront/Products/Show.php
app/Livewire/Storefront/Search/Index.php
app/Models/Cart.php
app/Models/CartLine.php
app/Models/Checkout.php
app/Models/Collection.php
app/Models/Concerns/BelongsToStore.php
app/Models/Customer.php
app/Models/CustomerAddress.php
app/Models/Discount.php
app/Models/Fulfillment.php
app/Models/FulfillmentLine.php
app/Models/InventoryItem.php
app/Models/NavigationItem.php
app/Models/NavigationMenu.php
app/Models/Order.php
app/Models/OrderLine.php
app/Models/Organization.php
app/Models/Page.php
app/Models/Payment.php
app/Models/Product.php
app/Models/ProductMedia.php
app/Models/ProductOption.php
app/Models/ProductOptionValue.php
app/Models/ProductVariant.php
app/Models/Refund.php
app/Models/Scopes/StoreScope.php
app/Models/ShippingRate.php
app/Models/ShippingZone.php
app/Models/Store.php
app/Models/StoreDomain.php
app/Models/StoreSettings.php
app/Models/StoreUser.php
app/Models/TaxSettings.php
app/Models/Theme.php
app/Models/ThemeFile.php
app/Models/ThemeSettings.php
app/Models/User.php
app/Policies/CollectionPolicy.php
app/Policies/CustomerPolicy.php
app/Policies/DiscountPolicy.php
app/Policies/FulfillmentPolicy.php
app/Policies/OrderPolicy.php
app/Policies/PagePolicy.php
app/Policies/ProductPolicy.php
app/Policies/RefundPolicy.php
app/Policies/StorePolicy.php
app/Policies/ThemePolicy.php
app/Providers/AppServiceProvider.php
app/Providers/FortifyServiceProvider.php
app/Services/CartService.php
app/Services/CheckoutService.php
app/Services/DiscountService.php
app/Services/FulfillmentService.php
app/Services/InventoryService.php
app/Services/NavigationService.php
app/Services/OrderService.php
app/Services/PaymentService.php
app/Services/Payments/MockPaymentProvider.php
app/Services/PricingEngine.php
app/Services/ProductService.php
app/Services/RefundService.php
app/Services/ShippingCalculator.php
app/Services/Tax/ManualTaxProvider.php
app/Services/Tax/StripeTaxProvider.php
app/Services/TaxCalculator.php
app/Services/ThemeSettingsService.php
app/Services/VariantMatrixService.php
app/Support/HandleGenerator.php
app/Support/Money.php
app/Support/OrderAccessToken.php
app/Traits/ChecksStoreRole.php
app/ValueObjects/DiscountResult.php
app/ValueObjects/PaymentResult.php
app/ValueObjects/PricingResult.php
app/ValueObjects/RefundResult.php
app/ValueObjects/TaxLine.php
app/ValueObjects/TaxResult.php
database/factories/CartFactory.php
database/factories/CartLineFactory.php
database/factories/CheckoutFactory.php
database/factories/CollectionFactory.php
database/factories/CustomerAddressFactory.php
database/factories/CustomerFactory.php
database/factories/DiscountFactory.php
database/factories/FulfillmentFactory.php
database/factories/FulfillmentLineFactory.php
database/factories/InventoryItemFactory.php
database/factories/NavigationItemFactory.php
database/factories/NavigationMenuFactory.php
database/factories/OrderFactory.php
database/factories/OrderLineFactory.php
database/factories/OrganizationFactory.php
database/factories/PageFactory.php
database/factories/PaymentFactory.php
database/factories/ProductFactory.php
database/factories/ProductMediaFactory.php
database/factories/ProductOptionFactory.php
database/factories/ProductOptionValueFactory.php
database/factories/ProductVariantFactory.php
database/factories/RefundFactory.php
database/factories/ShippingRateFactory.php
database/factories/ShippingZoneFactory.php
database/factories/StoreDomainFactory.php
database/factories/StoreFactory.php
database/factories/StoreSettingsFactory.php
database/factories/TaxSettingsFactory.php
database/factories/ThemeFactory.php
database/factories/ThemeFileFactory.php
database/factories/ThemeSettingsFactory.php
database/factories/UserFactory.php
database/migrations/0001_01_01_000000_create_users_table.php
database/migrations/0001_01_01_000001_create_cache_table.php
database/migrations/0001_01_01_000002_create_jobs_table.php
database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php
database/migrations/2026_05_03_211409_create_organizations_table.php
database/migrations/2026_05_03_211413_create_stores_table.php
database/migrations/2026_05_03_211416_create_store_domains_table.php
database/migrations/2026_05_03_211419_create_store_settings_table.php
database/migrations/2026_05_03_211425_create_store_users_table.php
database/migrations/2026_05_03_211556_create_customers_table.php
database/migrations/2026_05_03_212158_create_customer_password_reset_tokens_table.php
database/migrations/2026_05_03_213856_create_products_table.php
database/migrations/2026_05_03_213857_create_product_options_table.php
database/migrations/2026_05_03_213858_create_product_option_values_table.php
database/migrations/2026_05_03_213859_create_product_variants_table.php
database/migrations/2026_05_03_213900_create_variant_option_values_table.php
database/migrations/2026_05_03_213901_create_inventory_items_table.php
database/migrations/2026_05_03_213902_create_collections_table.php
database/migrations/2026_05_03_213903_create_collection_products_table.php
database/migrations/2026_05_03_213904_create_product_media_table.php
database/migrations/2026_05_03_223824_create_themes_table.php
database/migrations/2026_05_03_223830_create_theme_files_table.php
database/migrations/2026_05_03_223837_create_theme_settings_table.php
database/migrations/2026_05_03_223841_create_pages_table.php
database/migrations/2026_05_03_223844_create_navigation_menus_table.php
database/migrations/2026_05_03_223848_create_navigation_items_table.php
database/migrations/2026_05_03_225400_create_carts_table.php
database/migrations/2026_05_03_225405_create_cart_lines_table.php
database/migrations/2026_05_03_225409_create_checkouts_table.php
database/migrations/2026_05_03_225413_create_shipping_zones_table.php
database/migrations/2026_05_03_225418_create_shipping_rates_table.php
database/migrations/2026_05_03_225422_create_tax_settings_table.php
database/migrations/2026_05_03_225427_create_discounts_table.php
database/migrations/2026_05_04_000408_create_customer_addresses_table.php
database/migrations/2026_05_04_000408_create_orders_table.php
database/migrations/2026_05_04_000410_create_order_lines_table.php
database/migrations/2026_05_04_000411_create_payments_table.php
database/migrations/2026_05_04_000412_create_refunds_table.php
database/migrations/2026_05_04_000413_create_fulfillments_table.php
database/migrations/2026_05_04_000414_create_fulfillment_lines_table.php
database/seeders/CartLineSeeder.php
database/seeders/CartSeeder.php
database/seeders/CheckoutSeeder.php
database/seeders/CollectionSeeder.php
database/seeders/CustomerAddressSeeder.php
database/seeders/CustomerSeeder.php
database/seeders/DatabaseSeeder.php
database/seeders/DiscountSeeder.php
database/seeders/FulfillmentLineSeeder.php
database/seeders/FulfillmentSeeder.php
database/seeders/InventoryItemSeeder.php
database/seeders/NavigationItemSeeder.php
database/seeders/NavigationMenuSeeder.php
database/seeders/OrderLineSeeder.php
database/seeders/OrderSeeder.php
database/seeders/OrganizationSeeder.php
database/seeders/PageSeeder.php
database/seeders/PaymentSeeder.php
database/seeders/ProductMediaSeeder.php
database/seeders/ProductOptionSeeder.php
database/seeders/ProductOptionValueSeeder.php
database/seeders/ProductSeeder.php
database/seeders/ProductVariantSeeder.php
database/seeders/RefundSeeder.php
database/seeders/ShippingRateSeeder.php
database/seeders/ShippingZoneSeeder.php
database/seeders/StoreDomainSeeder.php
database/seeders/StoreSeeder.php
database/seeders/StoreSettingsSeeder.php
database/seeders/StoreUserSeeder.php
database/seeders/TaxSettingsSeeder.php
database/seeders/ThemeFileSeeder.php
database/seeders/ThemeSeeder.php
database/seeders/ThemeSettingsSeeder.php
database/seeders/UserSeeder.php
resources/views/components/action-message.blade.php
resources/views/components/app-logo-icon.blade.php
resources/views/components/app-logo.blade.php
resources/views/components/auth-header.blade.php
resources/views/components/auth-session-status.blade.php
resources/views/components/desktop-user-menu.blade.php
resources/views/components/placeholder-pattern.blade.php
resources/views/components/settings/layout.blade.php
resources/views/components/storefront/breadcrumbs.blade.php
resources/views/components/storefront/price.blade.php
resources/views/components/storefront/product-card.blade.php
resources/views/dashboard.blade.php
resources/views/flux/icon/book-open-text.blade.php
resources/views/flux/icon/chevrons-up-down.blade.php
resources/views/flux/icon/folder-git-2.blade.php
resources/views/flux/icon/layout-grid.blade.php
resources/views/flux/navlist/group.blade.php
resources/views/layouts/app.blade.php
resources/views/layouts/app/header.blade.php
resources/views/layouts/app/sidebar.blade.php
resources/views/layouts/auth.blade.php
resources/views/layouts/auth/card.blade.php
resources/views/layouts/auth/simple.blade.php
resources/views/layouts/auth/split.blade.php
resources/views/layouts/storefront.blade.php
resources/views/livewire/admin/auth/login.blade.php
resources/views/livewire/admin/collections/form.blade.php
resources/views/livewire/admin/collections/index.blade.php
resources/views/livewire/admin/customers/index.blade.php
resources/views/livewire/admin/customers/show.blade.php
resources/views/livewire/admin/dashboard.blade.php
resources/views/livewire/admin/discounts/form.blade.php
resources/views/livewire/admin/discounts/index.blade.php
resources/views/livewire/admin/inventory/index.blade.php
resources/views/livewire/admin/navigation/index.blade.php
resources/views/livewire/admin/orders/index.blade.php
resources/views/livewire/admin/orders/show.blade.php
resources/views/livewire/admin/pages/form.blade.php
resources/views/livewire/admin/pages/index.blade.php
resources/views/livewire/admin/products/form.blade.php
resources/views/livewire/admin/products/index.blade.php
resources/views/livewire/admin/settings/index.blade.php
resources/views/livewire/admin/settings/shipping.blade.php
resources/views/livewire/admin/settings/taxes.blade.php
resources/views/livewire/admin/themes/editor.blade.php
resources/views/livewire/admin/themes/index.blade.php
resources/views/livewire/auth/confirm-password.blade.php
resources/views/livewire/auth/forgot-password.blade.php
resources/views/livewire/auth/login.blade.php
resources/views/livewire/auth/register.blade.php
resources/views/livewire/auth/reset-password.blade.php
resources/views/livewire/auth/two-factor-challenge.blade.php
resources/views/livewire/auth/verify-email.blade.php
resources/views/livewire/settings/appearance.blade.php
resources/views/livewire/settings/delete-user-form.blade.php
resources/views/livewire/settings/password.blade.php
resources/views/livewire/settings/profile.blade.php
resources/views/livewire/settings/two-factor.blade.php
resources/views/livewire/settings/two-factor/recovery-codes.blade.php
resources/views/livewire/storefront/account/auth/login.blade.php
resources/views/livewire/storefront/account/auth/register.blade.php
resources/views/livewire/storefront/account/orders/index.blade.php
resources/views/livewire/storefront/account/orders/show.blade.php
resources/views/livewire/storefront/cart-drawer.blade.php
resources/views/livewire/storefront/cart/show.blade.php
resources/views/livewire/storefront/checkout/confirmation.blade.php
resources/views/livewire/storefront/checkout/show.blade.php
resources/views/livewire/storefront/collections/index.blade.php
resources/views/livewire/storefront/collections/show.blade.php
resources/views/livewire/storefront/home.blade.php
resources/views/livewire/storefront/pages/show.blade.php
resources/views/livewire/storefront/products/show.blade.php
resources/views/livewire/storefront/search/index.blade.php
resources/views/partials/head.blade.php
resources/views/partials/settings-heading.blade.php
resources/views/storefront/account/dashboard.blade.php
resources/views/welcome.blade.php
routes/api.php
routes/console.php
routes/settings.php
routes/web.php
tests/Feature/Admin/ContentManagementTest.php
tests/Feature/Admin/CustomerManagementTest.php
tests/Feature/Admin/DashboardTest.php
tests/Feature/Admin/DiscountManagementTest.php
tests/Feature/Admin/OrderManagementTest.php
tests/Feature/Admin/SettingsManagementTest.php
tests/Feature/Api/AdminOrderApiTest.php
tests/Feature/Api/StorefrontCartApiTest.php
tests/Feature/Api/StorefrontCheckoutApiTest.php
tests/Feature/Api/StorefrontOrderApiTest.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Auth/EmailVerificationTest.php
tests/Feature/Auth/PasswordConfirmationTest.php
tests/Feature/Auth/PasswordResetTest.php
tests/Feature/Auth/RegistrationTest.php
tests/Feature/Auth/TwoFactorChallengeTest.php
tests/Feature/Cart/CartServiceTest.php
tests/Feature/Catalog/CatalogFoundationTest.php
tests/Feature/Catalog/CatalogUiTest.php
tests/Feature/Catalog/InventoryServiceTest.php
tests/Feature/Catalog/MediaProcessingTest.php
tests/Feature/Catalog/ProductServiceTest.php
tests/Feature/Catalog/VariantMatrixServiceTest.php
tests/Feature/Checkout/CheckoutServiceTest.php
tests/Feature/Checkout/PricingServicesTest.php
tests/Feature/DashboardTest.php
tests/Feature/ExampleTest.php
tests/Feature/Foundation/AuthorizationTest.php
tests/Feature/Foundation/CustomerAuthTest.php
tests/Feature/Foundation/TenancyTest.php
tests/Feature/Orders/BankTransferOrderTest.php
tests/Feature/Orders/FulfillmentServiceTest.php
tests/Feature/Orders/OrderServiceTest.php
tests/Feature/Orders/RefundServiceTest.php
tests/Feature/Payments/MockPaymentProviderTest.php
tests/Feature/Settings/PasswordUpdateTest.php
tests/Feature/Settings/ProfileUpdateTest.php
tests/Feature/Settings/TwoFactorAuthenticationTest.php
tests/Feature/Storefront/CartCheckoutUiTest.php
tests/Feature/Storefront/OrderViewsTest.php
tests/Feature/Storefront/ThemeDataTest.php
tests/Pest.php
tests/TestCase.php
tests/Unit/ExampleTest.php


output:
Chunk ID: 55f846
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 857
Output:
| `Admin\Apps\Index` | App directory |
| `Admin\Apps\Show` | Installed app detail |
| `Admin\Developers\Index` | API token management, webhook subscriptions |

---

### Phase 8: Search

**Priority:** LOW - enhances product discovery.

#### Step 8.1: Migrations

1. `create_search_settings_table` - PK: `store_id`
2. `create_search_queries_table` - FK: `store_id`
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

#### Step 8.2: Search Service

**File to create:** `App\Services\SearchService`

| Method | Description |
|--------|-------------|
| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
| `syncProduct(Product $product): void` | Upsert into FTS5 index |
| `removeProduct(int $productId): void` | Delete from FTS5 index |

**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.

#### Step 8.3: Search UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |

---

### Phase 9: Analytics

**Priority:** LOW - reporting and insights.

#### Step 9.1: Migrations

1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

**File to create:** `App\Services\AnalyticsService`

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

**File to create:** `App\Jobs\AggregateAnalytics`

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

**File to create:** `App\Services\WebhookService`

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |


output:
Chunk ID: 6c6991
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1286
Output:

**Error responses:**
- `401` - Invalid or missing token
- `404` - Order not found

---

### 2.5 Search Endpoints

#### GET /api/storefront/v1/search

Search products within the current store.

- **Authentication:** None
- **Rate limit:** `search` (30/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | Yes | Search query. Min: 1, max: 200 characters. |
| `filters` | json | No | URL-encoded JSON object with filter facets. |
| `sort` | string | No | One of: `relevance`, `price_asc`, `price_desc`, `newest`, `best_selling`. Default: `relevance`. |
| `page` | integer | No | Page number. Default: 1. |
| `per_page` | integer | No | Results per page. Min: 1, max: 50. Default: 24. |

**Filters JSON schema:**

```json
{
  "collection_id": 5,
  "price_min": 1000,
  "price_max": 5000,
  "in_stock": true,
  "tags": ["organic", "cotton"],
  "vendor": "Acme Apparel"
}
```

**Success response:** `200 OK`

```json
{
  "query": "t-shirt",
  "results": [
    {
      "id": 10,
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "vendor": "Acme Apparel",
      "product_type": "Apparel",
      "price_amount": 2500,
      "compare_at_amount": 3500,
      "currency": "EUR",
      "image_url": "https://cdn.example.com/products/tshirt.jpg",
      "in_stock": true,
      "tags": ["organic", "cotton"]
    }
  ],
  "facets": {
    "vendors": [
      { "value": "Acme Apparel", "count": 12 }
    ],
    "tags": [
      { "value": "organic", "count": 8 },
      { "value": "cotton", "count": 15 }
    ],
    "price_range": {
      "min": 1500,
      "max": 8500
    }
  },
  "pagination": {
    "current_page": 1,
    "per_page": 24,
    "total": 42,
    "last_page": 2
  }
}
```

**Error responses:**
- `400` - Query too long (max 200 characters) or malformed filters
- `422` - Validation error

---

#### GET /api/storefront/v1/search/suggest

Autocomplete suggestions as the user types.

- **Authentication:** None
- **Rate limit:** `search` (30/min per IP)

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | Yes | Search prefix. Min: 1, max: 100 characters. |
| `limit` | integer | No | Max suggestions. Default: 5, max: 10. |

**Success response:** `200 OK`

```json
{
  "query": "t-sh",
  "suggestions": [
    {
      "type": "product",
      "title": "Classic T-Shirt",
      "handle": "classic-t-shirt",
      "image_url": "https://cdn.example.com/products/tshirt-thumb.jpg",
      "price_amount": 2500,
      "currency": "EUR"
    },
    {
      "type": "collection",
      "title": "T-Shirts",
      "handle": "t-shirts",
      "image_url": null
    }
  ]
}
```

**Error responses:**
- `400` - Query too long
- `422` - Validation error

---

### 2.6 Analytics Endpoints

#### POST /api/storefront/v1/analytics/events

Batch submit storefront analytics events.

- **Authentication:** None
- **Rate limit:** `analytics` (60/min per IP)

**Request body:**

```json
{
  "events": [
    {
      "type": "page_view",
      "session_id": "sess_abc123",
      "client_event_id": "evt_001",
      "properties": {
        "url": "/products/classic-t-shirt",
        "referrer": "https://google.com",
        "user_agent": "Mozilla/5.0..."
      },
      "occurred_at": "2026-02-11T10:00:00Z"
    },
    {
      "type": "add_to_cart",
      "session_id": "sess_abc123",
      "client_event_id": "evt_002",
      "properties": {
        "product_id": 10,
        "variant_id": 101,
        "quantity": 1,
        "price_amount": 2500,
        "currency": "EUR"
      },
      "occurred_at": "2026-02-11T10:01:00Z"
    }
  ]
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `events` | array | Yes | Min: 1, max: 50 events per batch. |
| `events[].type` | string | Yes | One of: `page_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. |
| `events[].session_id` | string | Yes | Max: 100 characters. |
| `events[].client_event_id` | string | Yes | Max: 100 characters. If a `client_event_id` has already been recorded for the current store, the duplicate event is silently dropped (unique on `(store_id, client_event_id)`). |
| `events[].properties` | object | No | Freeform JSON. Max depth: 3 levels. |
| `events[].occurred_at` | string (ISO 8601) | Yes | Must not be more than 1 hour in the past or in the future. |

**Success response:** `202 Accepted`

```json
{
  "accepted": 2,
  "rejected": 0
}
```

**Error responses:**
- `422` - Validation errors (invalid event types, too many events, etc.)

---

## 3. Admin REST API

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

These endpoints provide programmatic access for integrations, CLI tools, and third-party apps. The admin web UI uses Livewire actions directly and does not call these endpoints.

All endpoints require a valid Sanctum API token with appropriate scopes. Responses follow Laravel's API Resource conventions.


output:
Chunk ID: b0464d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1122
Output:
- Store all sizes with predictable path keys: `media/{product_id}/{media_id}/{size}.{ext}`
- Update the `product_media` record with: `width`, `height`, `mime_type`, `byte_size` from the original, and `status = 'ready'`.
- On failure: set `status = 'failed'` and log the error. Do not delete the original file.
- Maximum 3 retry attempts before permanent failure.

### 15.3 Cleanup

When a `product_media` record is deleted, a model event listener removes all associated files from storage (all sizes and the original).

---

## 16. Search (SQLite FTS5)

### 16.1 Index Setup

Create an FTS5 virtual table named `products_fts` with these columns:

| Column | Indexed | Description |
|---|---|---|
| `store_id` | No (UNINDEXED) | Used for filtering, not searching |
| `product_id` | No (UNINDEXED) | Foreign key reference for loading results |
| `title` | Yes | Product title, fully searchable |
| `description` | Yes | Plain-text version of description_html (HTML tags stripped) |
| `vendor` | Yes | Vendor name |
| `product_type` | Yes | Product type |
| `tags` | Yes | Space-separated tag values |

### 16.2 Index Synchronization

**Observer:** `App\Observers\ProductObserver`

| Model Event | FTS Action |
|---|---|
| `created` | Insert row into products_fts |
| `updated` | Delete old row, insert updated row (FTS5 does not support UPDATE) |
| `deleted` | Delete row from products_fts |

**Data preparation before insert:**
- Strip HTML tags from `description_html` to get plain text.
- Convert JSON tags array to space-separated string.
- Use empty string for null vendor and product_type fields.

### 16.3 Search Query

**Service:** `App\Services\SearchService`

**Search algorithm:**

```
PROCEDURE search(store, queryString, limit = 20):
    1. Sanitize queryString: escape FTS5 special characters
    2. Append '*' to last token for prefix matching (autocomplete)
       Example: "running sh" becomes "running sh*"
    3. Execute FTS5 MATCH query:
       - Filter by store_id
       - Order by FTS5 relevance rank
       - Limit results
    4. Load matching Product models
    5. Exclude products with status != 'active' or published_at IS NULL
    6. RETURN ordered collection
```

### 16.4 Search Logging

Each search query is logged to `search_queries` with: store_id, query text, results_count, created_at. This data feeds into analytics and can inform autocomplete suggestions.

---

## 17. Event and Listener Registry

Summary of all domain events and their listeners/side effects:

| Event | Listeners / Side Effects |
|---|---|
| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
| `CheckoutAddressed` | Recalculate pricing |
| `CheckoutShippingSelected` | Recalculate pricing |
| `CheckoutCompleted` | Dispatch webhooks (`checkout.completed`) |
| `CheckoutExpired` | Release reserved inventory |
| `ProductStatusChanged` | Sync FTS5 index if transitioning to/from active |
| `ProductCreated` | Sync FTS5 index, dispatch webhooks (`product.created`) |
| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (`product.updated`) |
| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (`product.deleted`) |
| `OrderPaid` | Update order status, commit inventory (bank transfer confirmation), auto-fulfill digital products |
| `FulfillmentCreated` | Update order fulfillment status |
| `FulfillmentShipped` | Send shipping notification email with tracking info, dispatch webhooks (`order.fulfilled` if fully fulfilled) |
| `FulfillmentDelivered` | Update fulfillment record, dispatch webhooks |
| `OrderCancelled` | Release reserved inventory, send cancellation email |

All events should be placed in `App\Events`. All listeners in `App\Listeners`.

---

## 18. Scheduled Jobs Registry

| Job Class | Schedule | Description |
|---|---|---|
| `App\Jobs\CleanupAbandonedCarts` | Daily | Mark stale active carts as abandoned (14-day threshold) |
| `App\Jobs\ExpireAbandonedCheckouts` | Every 15 minutes | Expire checkouts idle for 24+ hours, release reserved inventory |
| `App\Jobs\AggregateAnalytics` | Daily at 01:00 UTC | Roll up raw analytics events into daily aggregates |
| `App\Jobs\CancelUnpaidBankTransferOrders` | Daily | Cancel bank transfer orders that remain unpaid after configurable days (default: 7) |

**Queued jobs (dispatched on demand, not scheduled):**

| Job Class | Description |


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


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


cmd: sed -n '1,260p' routes/web.php && sed -n '1,260p' routes/api.php


cmd: sed -n '1,260p' app/Models/Product.php && sed -n '1,220p' app/Models/ProductVariant.php && sed -n '1,180p' app/Providers/AppServiceProvider.php


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

aggregated_output:
<?php

namespace App\Livewire\Storefront\Search;

use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public string $q = '';

    public function mount(): void
    {
        $this->q = (string) request('q', '');
    }

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

    public function products(): LengthAwarePaginator
    {
        return Product::query()
            ->with(['variants.inventoryItem'])
            ->withCount('variants')
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->when(trim($this->q) !== '', function (Builder $query): void {
                $search = '%'.trim($this->q).'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('title', 'like', $search)
                        ->orWhere('vendor', 'like', $search)
                        ->orWhere('product_type', 'like', $search)
                        ->orWhere('tags', 'like', $search);
                });
            })
            ->latest('published_at')
            ->paginate(12);
    }

    public function render(): mixed
    {
        return view('livewire.storefront.search.index', [
            'products' => $this->products(),
        ])->layout('layouts.storefront', [
            'title' => __('Search results'),
        ]);
    }
}


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

namespace App\Livewire\Storefront\Search;

use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public string $q = '';

    public function mount(): void
    {
        $this->q = (string) request('q', '');
    }

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

    public function products(): LengthAwarePaginator
    {
        return Product::query()
            ->with(['variants.inventoryItem'])
            ->withCount('variants')
            ->where('status', 'active')
            ->whereNotNull('published_at')
            ->when(trim($this->q) !== '', function (Builder $query): void {
                $search = '%'.trim($this->q).'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('title', 'like', $search)
                        ->orWhere('vendor', 'like', $search)
                        ->orWhere('product_type', 'like', $search)
                        ->orWhere('tags', 'like', $search);
                });
            })
            ->latest('published_at')
            ->paginate(12);
    }

    public function render(): mixed
    {
        return view('livewire.storefront.search.index', [
            'products' => $this->products(),
        ])->layout('layouts.storefront', [
            'title' => __('Search results'),
        ]);
    }
}


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

aggregated_output:
    <x-storefront.breadcrumbs :items="[['label' => 'Search results']]" />

            @if (trim($q) !== '')
                {{ $products->total() }} results for "{{ $q }}"
            @else
                Search products
            @endif

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

        @if ($products->isEmpty())

                    <flux:icon name="magnifying-glass" class="size-10 text-zinc-400" />

No products found

Try another search term or browse a collection.

                    <flux:button :href="route('collections.index')" wire:navigate variant="ghost">Browse collections</flux:button>

        @else

                @foreach ($products as $product)
                    <x-storefront.product-card :product="$product" wire:key="search-product-{{ $product->getKey() }}" />
                @endforeach

                {{ $products->links() }}

        @endif


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

    <x-storefront.breadcrumbs :items="[['label' => 'Search results']]" />

            @if (trim($q) !== '')
                {{ $products->total() }} results for "{{ $q }}"
            @else
                Search products
            @endif

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

        @if ($products->isEmpty())

                    <flux:icon name="magnifying-glass" class="size-10 text-zinc-400" />

No products found

Try another search term or browse a collection.

                    <flux:button :href="route('collections.index')" wire:navigate variant="ghost">Browse collections</flux:button>

        @else

                @foreach ($products as $product)
                    <x-storefront.product-card :product="$product" wire:key="search-product-{{ $product->getKey() }}" />
                @endforeach

                {{ $products->links() }}

        @endif


command: sed -n '1,260p' routes/web.php && sed -n '1,260p' routes/api.php

web.php

cmd: sed -n '1,260p' routes/web.php

api.php

cmd: sed -n '1,260p' routes/api.php

aggregated_output:
<?php

use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Livewire\Admin\Auth\Login as AdminLogin;
use App\Livewire\Admin\Collections\Form as AdminCollectionForm;
use App\Livewire\Admin\Collections\Index as AdminCollectionsIndex;
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\Navigation\Index as AdminNavigationIndex;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Livewire\Admin\Pages\Form as AdminPageForm;
use App\Livewire\Admin\Pages\Index as AdminPagesIndex;
use App\Livewire\Admin\Products\Form as AdminProductForm;
use App\Livewire\Admin\Products\Index as AdminProductsIndex;
use App\Livewire\Admin\Settings\Index as AdminSettingsIndex;
use App\Livewire\Admin\Settings\Shipping as AdminSettingsShipping;
use App\Livewire\Admin\Settings\Taxes as AdminSettingsTaxes;
use App\Livewire\Admin\Themes\Editor as AdminThemeEditor;
use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
use App\Livewire\Storefront\Account\Orders\Index as CustomerOrdersIndex;
use App\Livewire\Storefront\Account\Orders\Show as CustomerOrderShow;
use App\Livewire\Storefront\Cart\Show as StorefrontCartShow;
use App\Livewire\Storefront\Checkout\Confirmation as StorefrontCheckoutConfirmation;
use App\Livewire\Storefront\Checkout\Show as StorefrontCheckoutShow;
use App\Livewire\Storefront\Collections\Index as StorefrontCollectionsIndex;
use App\Livewire\Storefront\Collections\Show as StorefrontCollectionShow;
use App\Livewire\Storefront\Home as StorefrontHome;
use App\Livewire\Storefront\Pages\Show as StorefrontPageShow;
use App\Livewire\Storefront\Products\Show as StorefrontProductShow;
use App\Livewire\Storefront\Search\Index as StorefrontSearchIndex;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('/', StorefrontHome::class)->name('home');
    Route::livewire('collections', StorefrontCollectionsIndex::class)->name('collections.index');
    Route::livewire('collections/{handle}', StorefrontCollectionShow::class)->name('collections.show');
    Route::livewire('products/{handle}', StorefrontProductShow::class)->name('products.show');
    Route::livewire('cart', StorefrontCartShow::class)->name('cart.show');
    Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
    Route::livewire('search', StorefrontSearchIndex::class)->name('search.index');
    Route::livewire('pages/{handle}', StorefrontPageShow::class)->name('pages.show');
});

Route::livewire('admin/login', AdminLogin::class)
    ->middleware('guest')
    ->name('admin.login');

Route::post('admin/logout', function () {
    Auth::guard('web')->logout();

    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->middleware('auth')->name('admin.logout');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    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('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    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');
});

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('account/login', CustomerLogin::class)
        ->middleware('guest:customer')
        ->name('account.login');

    Route::livewire('account/register', CustomerRegister::class)
        ->middleware('guest:customer')
        ->name('account.register');

    Route::livewire('account', CustomerOrdersIndex::class)
        ->middleware('auth:customer')
        ->name('account.dashboard');

    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
        ->middleware('auth:customer')
        ->name('account.orders.show');
});

Route::redirect('dashboard', 'admin')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';
<?php

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;
use App\Http\Controllers\Api\Storefront\V1\CartController;
use App\Http\Controllers\Api\Storefront\V1\CartLineController;
use App\Http\Controllers\Api\Storefront\V1\CheckoutController;
use App\Http\Controllers\Api\Storefront\V1\OrderController as StorefrontOrderController;
use Illuminate\Support\Facades\Route;

Route::middleware('store.resolve')
    ->prefix('storefront/v1')
    ->name('api.storefront.v1.')
    ->group(function (): void {
        Route::middleware('throttle:api.storefront')->group(function (): void {
            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
        });

        Route::middleware('throttle:checkout')->group(function (): void {
            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
            Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show'])->name('orders.show');
        });
    });

Route::middleware(['auth', 'throttle:60,1'])
    ->prefix('admin/v1/stores/{store}')
    ->name('api.admin.v1.')
    ->group(function (): void {
        Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
        Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
        Route::post('orders/{order}/fulfillments', [AdminOrderFulfillmentController::class, 'store'])->name('orders.fulfillments.store');
        Route::post('orders/{order}/refunds', [AdminOrderRefundController::class, 'store'])->name('orders.refunds.store');
    });


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

use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Livewire\Admin\Auth\Login as AdminLogin;
use App\Livewire\Admin\Collections\Form as AdminCollectionForm;
use App\Livewire\Admin\Collections\Index as AdminCollectionsIndex;
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\Navigation\Index as AdminNavigationIndex;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Livewire\Admin\Pages\Form as AdminPageForm;
use App\Livewire\Admin\Pages\Index as AdminPagesIndex;
use App\Livewire\Admin\Products\Form as AdminProductForm;
use App\Livewire\Admin\Products\Index as AdminProductsIndex;
use App\Livewire\Admin\Settings\Index as AdminSettingsIndex;
use App\Livewire\Admin\Settings\Shipping as AdminSettingsShipping;
use App\Livewire\Admin\Settings\Taxes as AdminSettingsTaxes;
use App\Livewire\Admin\Themes\Editor as AdminThemeEditor;
use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
use App\Livewire\Storefront\Account\Orders\Index as CustomerOrdersIndex;
use App\Livewire\Storefront\Account\Orders\Show as CustomerOrderShow;
use App\Livewire\Storefront\Cart\Show as StorefrontCartShow;
use App\Livewire\Storefront\Checkout\Confirmation as StorefrontCheckoutConfirmation;
use App\Livewire\Storefront\Checkout\Show as StorefrontCheckoutShow;
use App\Livewire\Storefront\Collections\Index as StorefrontCollectionsIndex;
use App\Livewire\Storefront\Collections\Show as StorefrontCollectionShow;
use App\Livewire\Storefront\Home as StorefrontHome;
use App\Livewire\Storefront\Pages\Show as StorefrontPageShow;
use App\Livewire\Storefront\Products\Show as StorefrontProductShow;
use App\Livewire\Storefront\Search\Index as StorefrontSearchIndex;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('/', StorefrontHome::class)->name('home');
    Route::livewire('collections', StorefrontCollectionsIndex::class)->name('collections.index');
    Route::livewire('collections/{handle}', StorefrontCollectionShow::class)->name('collections.show');
    Route::livewire('products/{handle}', StorefrontProductShow::class)->name('products.show');
    Route::livewire('cart', StorefrontCartShow::class)->name('cart.show');
    Route::livewire('checkout', StorefrontCheckoutShow::class)->name('checkout.show');
    Route::livewire('checkout/confirmation/{order}', StorefrontCheckoutConfirmation::class)->name('checkout.confirmation');
    Route::livewire('search', StorefrontSearchIndex::class)->name('search.index');
    Route::livewire('pages/{handle}', StorefrontPageShow::class)->name('pages.show');
});

Route::livewire('admin/login', AdminLogin::class)
    ->middleware('guest')
    ->name('admin.login');

Route::post('admin/logout', function () {
    Auth::guard('web')->logout();

    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->middleware('auth')->name('admin.logout');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    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('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    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');
});

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('account/login', CustomerLogin::class)
        ->middleware('guest:customer')
        ->name('account.login');

    Route::livewire('account/register', CustomerRegister::class)
        ->middleware('guest:customer')
        ->name('account.register');

    Route::livewire('account', CustomerOrdersIndex::class)
        ->middleware('auth:customer')
        ->name('account.dashboard');

    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
        ->middleware('auth:customer')
        ->name('account.orders.show');
});

Route::redirect('dashboard', 'admin')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';
<?php

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;
use App\Http\Controllers\Api\Storefront\V1\CartController;
use App\Http\Controllers\Api\Storefront\V1\CartLineController;
use App\Http\Controllers\Api\Storefront\V1\CheckoutController;
use App\Http\Controllers\Api\Storefront\V1\OrderController as StorefrontOrderController;
use Illuminate\Support\Facades\Route;

Route::middleware('store.resolve')
    ->prefix('storefront/v1')
    ->name('api.storefront.v1.')
    ->group(function (): void {
        Route::middleware('throttle:api.storefront')->group(function (): void {
            Route::post('carts', [CartController::class, 'store'])->name('carts.store');
            Route::get('carts/{cart}', [CartController::class, 'show'])->name('carts.show');
            Route::post('carts/{cart}/lines', [CartLineController::class, 'store'])->name('carts.lines.store');
            Route::put('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'update'])->name('carts.lines.update');
            Route::delete('carts/{cart}/lines/{cartLine}', [CartLineController::class, 'destroy'])->name('carts.lines.destroy');
        });

        Route::middleware('throttle:checkout')->group(function (): void {
            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
            Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show'])->name('orders.show');
        });
    });

Route::middleware(['auth', 'throttle:60,1'])
    ->prefix('admin/v1/stores/{store}')
    ->name('api.admin.v1.')
    ->group(function (): void {
        Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
        Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
        Route::post('orders/{order}/fulfillments', [AdminOrderFulfillmentController::class, 'store'])->name('orders.fulfillments.store');
        Route::post('orders/{order}/refunds', [AdminOrderRefundController::class, 'store'])->name('orders.refunds.store');
    });


command: sed -n '1,260p' app/Models/Product.php && sed -n '1,220p' app/Models/ProductVariant.php && sed -n '1,180p' app/Providers/AppServiceProvider.php

Product.php

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

AppServiceProvider.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\ProductStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'title',
        'handle',
        'status',
        'description_html',
        'vendor',
        'product_type',
        'tags',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
        'tags' => '[]',
    ];

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

    /**
     * @return HasMany<ProductOption, $this>
     */
    public function options(): HasMany
    {
        return $this->hasMany(ProductOption::class)->orderBy('position');
    }

    /**
     * @return HasMany<ProductVariant, $this>
     */
    public function variants(): HasMany
    {
        return $this->hasMany(ProductVariant::class)->orderBy('position');
    }

    /**
     * @return HasMany<ProductMedia, $this>
     */
    public function media(): HasMany
    {
        return $this->hasMany(ProductMedia::class)->orderBy('position');
    }

    /**
     * @return BelongsToMany<Collection, $this>
     */
    public function collections(): BelongsToMany
    {
        return $this->belongsToMany(Collection::class, 'collection_products')
            ->withPivot('position')
            ->orderByPivot('position');
    }

    public function isPublished(): bool
    {
        return $this->status === ProductStatus::Active && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ProductStatus::class,
            'tags' => 'array',
            'published_at' => 'datetime',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use RuntimeException;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::saving(function (ProductVariant $variant): void {
            $variant->assertSkuIsUniqueForStore();
        });

        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = Product::withoutGlobalScopes()
                ->whereKey($variant->product_id)
                ->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ]);
        });
    }

    private function assertSkuIsUniqueForStore(): void
    {
        $sku = trim((string) $this->sku);

        if ($sku === '') {
            return;
        }

        $storeId = P…1012 chars truncated…ass, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}
<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Contracts\PaymentProvider;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Http\Middleware\ResolveStore;
use App\Models\Store;
use App\Services\NavigationService;
use App\Services\Payments\MockPaymentProvider;
use App\Services\ThemeSettingsService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View as ViewInstance;
use Livewire\Livewire;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(PaymentProvider::class, MockPaymentProvider::class);

        Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
            return new CustomerUserProvider($app['hash'], $config['model']);
        });

        $this->app->singleton(ThemeSettingsService::class);
        $this->app->singleton(NavigationService::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
        $this->configureLivewireMiddleware();
        $this->configureStorefrontViewData();
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );

        RateLimiter::for('api.storefront', function (Request $request): Limit {
            return Limit::perMinute(120)->by($request->ip());
        });

        RateLimiter::for('checkout', function (Request $request): Limit {
            $sessionId = $request->hasSession() ? $request->session()->getId() : null;

            return Limit::perMinute(10)->by($sessionId ?: $request->ip());
        });

        Authenticate::redirectUsing(function (Request $request): string {
            if ($request->is('admin*')) {
                return route('admin.login');
            }

            if ($request->is('account*')) {
                return route('account.login');
            }

            return route('login');
        });
    }

    protected function configureStorefrontViewData(): void
    {
        View::composer('layouts.storefront', function (ViewInstance $view): void {
            $store = app()->bound('current_store') ? app('current_store') : null;

            if (! $store instanceof Store) {
                $view->with([
                    'themeSettings' => [],
                    'mainNavigation' => [],
                    'footerNavigation' => [],
                ]);

                return;
            }

            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
            $navigation = app(NavigationService::class);

            $view->with([
                'themeSettings' => $themeSettings,
                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
            ]);
        });
    }

    protected function configureLivewireMiddleware(): void
    {
        Livewire::addPersistentMiddleware([
            EnsureUserEmailIsVerified::class,
            ResolveStore::class,
            CheckStoreRole::class,
        ]);
    }
}


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

namespace App\Models;

use App\Enums\ProductStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'title',
        'handle',
        'status',
        'description_html',
        'vendor',
        'product_type',
        'tags',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
        'tags' => '[]',
    ];

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

    /**
     * @return HasMany<ProductOption, $this>
     */
    public function options(): HasMany
    {
        return $this->hasMany(ProductOption::class)->orderBy('position');
    }

    /**
     * @return HasMany<ProductVariant, $this>
     */
    public function variants(): HasMany
    {
        return $this->hasMany(ProductVariant::class)->orderBy('position');
    }

    /**
     * @return HasMany<ProductMedia, $this>
     */
    public function media(): HasMany
    {
        return $this->hasMany(ProductMedia::class)->orderBy('position');
    }

    /**
     * @return BelongsToMany<Collection, $this>
     */
    public function collections(): BelongsToMany
    {
        return $this->belongsToMany(Collection::class, 'collection_products')
            ->withPivot('position')
            ->orderByPivot('position');
    }

    public function isPublished(): bool
    {
        return $this->status === ProductStatus::Active && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ProductStatus::class,
            'tags' => 'array',
            'published_at' => 'datetime',
        ];
    }
}
<?php

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use RuntimeException;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::saving(function (ProductVariant $variant): void {
            $variant->assertSkuIsUniqueForStore();
        });

        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = Product::withoutGlobalScopes()
                ->whereKey($variant->product_id)
                ->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ]);
        });
    }

    private function assertSkuIsUniqueForStore(): void
    {
        $sku = trim((string) $this->sku);

        if ($sku === '') {
            return;
        }

        $storeId = Product::withoutGlobalScopes()
            ->whereKey($this->product_id)
            ->value('store_id');

        if ($storeId === null) {
            return;
        }

        $query = self::withoutGlobalScopes()
            ->where('sku', $sku)
            ->whereHas('product', function (Builder $query) use ($storeId): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $storeId);
            });

        if ($this->exists) {
            $query->whereKeyNot($this->getKey());
        }

        if ($query->exists()) {
            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
        }
    }

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

    /**
     * @return HasOne<InventoryItem, $this>
     */
    public function inventoryItem(): HasOne
    {
        return $this->hasOne(InventoryItem::class, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}
<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Contracts\PaymentProvider;
use App\Http\Middleware\CheckStoreRole;
use App\Http\Middleware\EnsureUserEmailIsVerified;
use App\Http\Middleware\ResolveStore;
use App\Models\Store;
use App\Services\NavigationService;
use App\Services\Payments\MockPaymentProvider;
use App\Services\ThemeSettingsService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View as ViewInstance;
use Livewire\Livewire;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(PaymentProvider::class, MockPaymentProvider::class);

        Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
            return new CustomerUserProvider($app['hash'], $config['model']);
        });

        $this->app->singleton(ThemeSettingsService::class);
        $this->app->singleton(NavigationService::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
        $this->configureLivewireMiddleware();
        $this->configureStorefrontViewData();
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );

        RateLimiter::for('api.storefront', function (Request $request): Limit {
            return Limit::perMinute(120)->by($request->ip());
        });

        RateLimiter::for('checkout', function (Request $request): Limit {
            $sessionId = $request->hasSession() ? $request->session()->getId() : null;

            return Limit::perMinute(10)->by($sessionId ?: $request->ip());
        });

        Authenticate::redirectUsing(function (Request $request): string {
            if ($request->is('admin*')) {
                return route('admin.login');
            }

            if ($request->is('account*')) {
                return route('account.login');
            }

            return route('login');
        });
    }

    protected function configureStorefrontViewData(): void
    {
        View::composer('layouts.storefront', function (ViewInstance $view): void {
            $store = app()->bound('current_store') ? app('current_store') : null;

            if (! $store instanceof Store) {
                $view->with([
                    'themeSettings' => [],
                    'mainNavigation' => [],
                    'footerNavigation' => [],
                ]);

                return;
            }

            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
            $navigation = app(NavigationService::class);

            $view->with([
                'themeSettings' => $themeSettings,
                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
            ]);
        });
    }

    protected function configureLivewireMiddleware(): void
    {
        Livewire::addPersistentMiddleware([
            EnsureUserEmailIsVerified::class,
            ResolveStore::class,
            CheckStoreRole::class,
        ]);
    }
}


cmd: sed -n '1,140p' /Users/fabianwesner/Herd/shop/.agents/skills/livewire-development/SKILL.md


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


cmd: sed -n '1,140p' /Users/fabianwesner/Herd/shop/.agents/skills/tailwindcss-development/SKILL.md