userType: external

gitBranch: claude-subagents

slug: wise-shimmying-pretzel

type: message

role: assistant

service_tier: standard

speed: standard

type: assistant


type: user

role: user

toolUseResult: Error: File content (27031 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.


type: progress

hookEvent: PostToolUse

command: callback


content:
     1→# 03 - Admin Panel UI/UX Specification
     2→
     3→This document defines every view, component, and interaction for the admin panel. The admin is entirely server-rendered using Laravel 12 + Livewire v4 + Flux UI Free components + Tailwind CSS v4. There is no SPA or client-side router.
     4→
     5→**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
     6→
     7→Anything not covered by Flux UI Free uses plain Tailwind CSS v4 markup with Livewire directives.
     8→
     9→---
    10→
    11→## 1. Admin Layout Shell
    12→
    13→The admin layout is a persistent shell that wraps every admin page. It consists of a left sidebar, a top bar, and a main content area.
    14→
    15→### 1.1 Route Prefix
    16→
    17→All admin routes live under `/admin` and are protected by authentication middleware plus store-scoping middleware. The store is resolved from the session or a store-selector in the top bar.
    18→
    19→### 1.2 Sidebar Navigation
    20→
    21→**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`
    22→
    23→**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`
    24→
    25→#### Component Requirements
    26→
    27→| Property | Type | Purpose |
    28→|----------|------|---------|
    29→| `collapsed` | `bool` | Whether the sidebar is collapsed (mobile state) |
    30→| `currentRoute` | `string` | The current route name for active highlighting |
    31→
    32→| Action | Behavior |
    33→|--------|----------|
    34→| `toggle()` | Toggles the `collapsed` state |
    35→
    36→#### Wireframe
    37→
    38→```
    39→+-------------------------------+
    40→|  [BRAND LOGO]  Platform Name  |
    41→|-------------------------------|
    42→|  [separator]                  |
    43→|                               |
    44→|  [chart-bar]    Dashboard     |
    45→|                               |
    46→|  PRODUCTS                     |
    47→|  [cube]         Products      |
    48→|  [rect-stack]   Collections   |
    49→|  [archive-box]  Inventory     |
    50→|                               |
    51→|  [separator]                  |
    52→|  ORDERS                       |
    53→|  [shopping-bag] Orders        |
    54→|                               |
    55→|  [separator]                  |
    56→|  CUSTOMERS                    |
    57→|  [users]        Customers     |
    58→|                               |
    59→|  [separator]                  |
    60→|  DISCOUNTS                    |
    61→|  [tag]          Discounts     |
    62→|                               |
    63→|  [separator]                  |
    64→|  CONTENT                      |
    65→|  [doc-text]     Pages         |
    66→|  [bars-3]       Navigation    |
    67→|  [paint-brush]  Themes        |
    68→|                               |
    69→|  [separator]                  |
    70→|  [chart-pie]    Analytics     |
    71→|  [cog-6-tooth]  Settings      |
    72→|  [squares-2x2]  Apps          |
    73→|  [code-bracket] Developers    |
    74→+-------------------------------+
    75→```
    76→
    77→#### Navigation Structure
    78→
    79→```
    80→Dashboard                        -> admin.dashboard
    81→Products (group label)
    82→  Products                       -> admin.products.index
    83→  Collections                    -> admin.collections.index
    84→  Inventory                      -> admin.inventory.index
    85→Orders
    86→  Orders                         -> admin.orders.index
    87→Customers
    88→  Customers                      -> admin.customers.index
    89→Discounts
    90→  Discounts                      -> admin.discounts.index
    91→Content (group label)
    92→  Pages                          -> admin.pages.index
    93→  Navigation                     -> admin.navigation.index
    94→  Themes                         -> admin.themes.index
    95→Analytics                        -> admin.analytics.index
    96→Settings                         -> admin.settings.index
    97→Apps                             -> admin.apps.index
    98→Developers                       -> admin.developers.index
    99→```
   100→
   101→Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.
   102→
   103→#### Flux Component Mapping
   104→
   105→| UI Element | Flux Component |
   106→|-----------|----------------|
   107→| Platform logo and name | `brand` |
   108→| Navigation icons | `icon` |
   109→| Group dividers | `separator` |
   110→
   111→#### Responsive Behavior
   112→
   113→- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
   114→- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.
   115→
   116→### 1.3 Top Bar
   117→
   118→**Livewire component:** `App\Livewire\Admin\Layout\TopBar`
   119→
   120→**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`
   121→
   122→#### Component Requirements
   123→
   124→| Property | Type | Purpose |
   125→|----------|------|---------|
   126→| `currentStoreName` | `string` | Name of the active store |
   127→| `stores` | `Collection` | List of stores the user has access to |
   128→| `unreadNotificationCount` | `int` | Count of unread notifications |
   129→
   130→| Action | Behavior |
   131→|--------|----------|
   132→| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |
   133→
   134→#### Wireframe
   135→
   136→```
   137→+--------------------------------------------------------------------+
   138→| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
   139→+--------------------------------------------------------------------+
   140→  * hamburger visible on mobile only
   141→
   142→  Store Name dropdown:
   143→    - Store A
   144→    - Store B
   145→    - Store C
   146→
   147→  Avatar dropdown:
   148→    - Settings (links to /admin/settings)
   149→    - [separator]
   150→    - Log out (POST /logout with CSRF)
   151→```
   152→
   153→#### Interaction Flows
   154→
   155→- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
   156→- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
   157→- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.
   158→
   159→#### Flux Component Mapping
   160→
   161→| UI Element | Flux Component |
   162→|-----------|----------------|
   163→| Store selector | `dropdown` |
   164→| Hamburger (mobile) | `button` (icon: bars-3) |
   165→| Notification bell | `button` (icon: bell, variant: ghost) |
   166→| Notification count | `badge` |
   167→| User profile trigger | `profile` |
   168→| Profile menu | `dropdown` |
   169→| Menu divider | `separator` |
   170→
   171→### 1.4 Main Content Area
   172→
   173→The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.
   174→
   175→**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".
   176→
   177→**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.
   178→
   179→### 1.5 Toast Notifications
   180→
   181→A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.
   182→
   183→| Property | Value |
   184→|----------|-------|
   185→| Event name | `toast` |
   186→| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
   187→| Position | Top-right corner |
   188→| Auto-dismiss | 5 seconds |
   189→| Stacking | Supports multiple toasts |
   190→| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |
   191→
   192→Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").
   193→
   194→### 1.6 Dark Mode
   195→
   196→The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.
   197→
   198→Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.
   199→
   200→---

type: text

content:
# 03 - Admin Panel UI/UX Specification

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

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

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

---

## 1. Admin Layout Shell

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

### 1.1 Route Prefix

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

### 1.2 Sidebar Navigation

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

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

#### Component Requirements

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

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

#### Wireframe

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

#### Navigation Structure

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

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

#### Flux Component Mapping

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

#### Responsive Behavior

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

### 1.3 Top Bar

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

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

#### Component Requirements

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

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

#### Wireframe

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

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

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

#### Interaction Flows

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

#### Flux Component Mapping

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

### 1.4 Main Content Area

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

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

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

### 1.5 Toast Notifications

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

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

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

### 1.6 Dark Mode

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

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

---


content:
   100→#### Step 1.2: Core Migrations (Batch 1-2)
   101→
   102→Create in this dependency order:
   103→
   104→1. `create_organizations_table` - no dependencies
   105→2. `create_stores_table` - FK: `organization_id`
   106→3. `create_store_domains_table` - FK: `store_id`
   107→4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
   108→5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
   109→6. `create_store_settings_table` - PK: `store_id`
   110→
   111→All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.
   112→
   113→#### Step 1.3: Core Models
   114→
   115→**Files to create:**
   116→
   117→| Type | Name | Purpose |
   118→|------|------|---------|
   119→| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
   120→| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
   121→| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
   122→| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
   123→| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |
   124→
   125→**Relationship definitions:**
   126→
   127→| Model | Key Relationships |
   128→|-------|-------------------|
   129→| `Organization` | `hasMany(Store::class)` |
   130→| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
   131→| `StoreDomain` | `belongsTo(Store::class)` |
   132→| `User` | `belongsToMany(Store::class)` through `store_users` |
   133→| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
   134→| `StoreSettings` | `belongsTo(Store::class)` |
   135→
   136→Each model must have:
   137→- Explicit return type declarations on all methods
   138→- Proper `$fillable` or `$guarded` arrays
   139→- `casts()` method for JSON/enum columns
   140→- Factory with meaningful default values
   141→- Seeder that creates sample data
   142→
   143→#### Step 1.4: Enums
   144→
   145→**Files to create in `app/Enums/`:**
   146→
   147→| Enum | Values |
   148→|------|--------|
   149→| `StoreStatus` | `Active`, `Suspended` |
   150→| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
   151→| `StoreDomainType` | `Storefront`, `Admin`, `Api` |
   152→
   153→#### Step 1.5: Tenant Resolution Middleware
   154→
   155→**File to create:** `App\Http\Middleware\ResolveStore`
   156→
   157→Requirements:
   158→1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
   159→2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
   160→3. Return 404 if hostname not found
   161→4. Return 503 for suspended stores on storefront routes
   162→
   163→**Registration in `bootstrap/app.php`:**
   164→- Register middleware group `storefront` containing `ResolveStore`
   165→- Register middleware group `admin` containing `ResolveStore`
   166→
   167→#### Step 1.6: BelongsToStore Trait and Global Scope
   168→
   169→**Files to create:**
   170→
   171→| Type | Name | Purpose |
   172→|------|------|---------|
   173→| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
   174→| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |
   175→
   176→Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
   177→
   178→#### Step 1.7: Authentication
   179→
   180→**Admin auth requirements:**
   181→
   182→| Component | Description |
   183→|-----------|-------------|
   184→| Livewire component `Admin\Auth\Login` | Admin login form |
   185→| Livewire component `Admin\Auth\Logout` | Admin logout action |
   186→| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
   187→| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
   188→| Password reset | Standard Laravel flow with `Password::broker('users')` |
   189→
   190→**Customer auth requirements:**
   191→
   192→| Component | Description |
   193→|-----------|-------------|
   194→| Custom guard | `customer` guard with `customers` provider |
   195→| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
   196→| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
   197→| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
   198→| Uniqueness | Customer email unique per store, not globally |
   199→| Rate limiting | 5 attempts per minute per IP |
   200→
   201→**Rate limiter registration:**
   202→- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)
   203→
   204→#### Step 1.8: Authorization
   205→
   206→**Files to create:**
   207→
   208→| Type | Name | Purpose |
   209→|------|------|---------|
   210→| Policy | `ProductPolicy` | Product authorization based on store role |
   211→| Policy | `OrderPolicy` | Order authorization based on store role |
   212→| Policy | `CollectionPolicy` | Collection authorization based on store role |
   213→| Policy | `DiscountPolicy` | Discount authorization based on store role |
   214→| Policy | `CustomerPolicy` | Customer authorization based on store role |
   215→| Policy | `StorePolicy` | Store authorization based on store role |
   216→| Policy | `PagePolicy` | Page authorization based on store role |
   217→| Policy | `ThemePolicy` | Theme authorization based on store role |
   218→| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
   219→| Policy | `RefundPolicy` | Refund authorization based on store role |
   220→
   221→Each policy checks user role via `store_users` pivot for the current store. Helper on User model: `roleForStore(Store $store): ?StoreUserRole`. Register policies via auto-discovery or explicit binding in `AppServiceProvider`.
   222→
   223→**Permission Matrix:**
   224→
   225→| Permission | Owner | Admin | Staff | Support |
   226→|------------|:-----:|:-----:|:-----:|:-------:|
   227→| Manage store settings | Y | Y | N | N |
   228→| Delete store | Y | N | N | N |
   229→| Manage staff | Y | Y | N | N |
   230→| Manage products (CRUD) | Y | Y | Y | N |
   231→| Delete/archive products | Y | Y | N | N |
   232→| Manage orders | Y | Y | Y | Read-only |
   233→| Process refunds | Y | Y | N | N |
   234→| Create fulfillments | Y | Y | Y | N |
   235→| Manage discounts | Y | Y | Y | N |
   236→| Manage themes | Y | Y | N | N |
   237→| Manage apps | Y | Y | N | N |
   238→| View analytics | Y | Y | Y | N |
   239→| Manage shipping/tax | Y | Y | N | N |
   240→| View customers | Y | Y | Y | Y |
   241→| Update customers | Y | Y | Y | N |
   242→
   243→---
   244→
   245→### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
   246→
   247→**Priority:** HIGH - storefront and orders depend on this.
   248→
   249→#### Step 2.1: Migrations (Batch 3-5)
   250→
   251→Create in dependency order:
   252→
   253→1. `create_products_table` - FK: `store_id`
   254→2. `create_product_options_table` - FK: `product_id`
   255→3. `create_product_option_values_table` - FK: `product_option_id`
   256→4. `create_product_variants_table` - FK: `product_id`
   257→5. `create_variant_option_values_table` - composite PK: `variant_id` + `product_option_value_id`
   258→6. `create_inventory_items_table` - FK: `store_id`, `variant_id`
   259→7. `create_collections_table` - FK: `store_id`
   260→8. `create_collection_products_table` - composite PK: `collection_id` + `product_id`
   261→9. `create_product_media_table` - FK: `product_id`
   262→
   263→#### Step 2.2: Models with Relationships
   264→
   265→**Files to create:**
   266→
   267→| Type | Name | Purpose |
   268→|------|------|---------|
   269→| Model (with migration, factory, seeder) | `Product` | Product entity |
   270→| Model (with migration, factory, seeder) | `ProductOption` | Product option (e.g., Size, Color) |
   271→| Model (with migration, factory, seeder) | `ProductOptionValue` | Option values (e.g., S, M, L) |
   272→| Model (with migration, factory, seeder) | `ProductVariant` | Product variant (SKU-level) |
   273→| Model (with migration, factory, seeder) | `InventoryItem` | Inventory tracking per variant |
   274→| Model (with migration, factory, seeder) | `Collection` | Product collection/category |
   275→| Model (with migration, factory, seeder) | `ProductMedia` | Product images and videos |
   276→
   277→**Relationship definitions:**
   278→
   279→| Model | Key Relationships |
   280→|-------|-------------------|
   281→| `Product` | `hasMany(ProductVariant)`, `hasMany(ProductOption)`, `hasMany(ProductMedia)`, `belongsToMany(Collection)` via `collection_products` |
   282→| `ProductOption` | `belongsTo(Product)`, `hasMany(ProductOptionValue)` |
   283→| `ProductOptionValue` | `belongsTo(ProductOption)` |
   284→| `ProductVariant` | `belongsTo(Product)`, `hasOne(InventoryItem)`, `belongsToMany(ProductOptionValue)` via `variant_option_values` |
   285→| `InventoryItem` | `belongsTo(ProductVariant)` |
   286→| `Collection` | `belongsToMany(Product)` via `collection_products` |
   287→| `ProductMedia` | `belongsTo(Product)` |
   288→
   289→**Enums to create:**
   290→
   291→| Enum | Values |
   292→|------|--------|
   293→| `ProductStatus` | `Draft`, `Active`, `Archived` |
   294→| `VariantStatus` | `Active`, `Archived` |
   295→| `CollectionStatus` | `Draft`, `Active`, `Archived` |
   296→| `MediaType` | `Image`, `Video` |
   297→| `MediaStatus` | `Processing`, `Ready`, `Failed` |
   298→| `InventoryPolicy` | `Deny`, `Continue` |
   299→
   300→#### Step 2.3: Product Service
   301→
   302→**File to create:** `App\Services\ProductService`
   303→
   304→| Method | Description |
   305→|--------|-------------|
   306→| `create(Store $store, array $data): Product` | Creates product with nested variants/options |
   307→| `update(Product $product, array $data): Product` | Updates product |
   308→| `transitionStatus(Product $product, ProductStatus $newStatus): void` | Validates transitions per state machine |
   309→| `delete(Product $product): void` | Hard delete only if draft with no order references; otherwise throws |
   310→
   311→**File to create:** `App\Services\VariantMatrixService`
   312→
   313→| Method | Description |
   314→|--------|-------------|
   315→| `rebuildMatrix(Product $product): void` | Computes cartesian product, creates missing variants, archives orphaned ones |
   316→
   317→**File to create:** `App\Support\HandleGenerator`
   318→
   319→| Method | Description |
   320→|--------|-------------|
   321→| `generate(string $title, string $table, int $storeId, ?int $excludeId = null): string` | Generates unique slugs |
   322→
   323→#### Step 2.4: Inventory Service
   324→
   325→**File to create:** `App\Services\InventoryService`
   326→
   327→| Method | Description |
   328→|--------|-------------|
   329→| `checkAvailability(InventoryItem $item, int $quantity): bool` | Checks if sufficient available inventory exists |
   330→| `reserve(InventoryItem $item, int $quantity): void` | Increments `quantity_reserved` |
   331→| `release(InventoryItem $item, int $quantity): void` | Decrements `quantity_reserved` |
   332→| `commit(InventoryItem $item, int $quantity): void` | Decrements both `on_hand` and `reserved` |
   333→| `restock(InventoryItem $item, int $quantity): void` | Increments `quantity_on_hand` |
   334→
   335→All operations wrapped in database transactions. Throws `InsufficientInventoryException` when policy is `deny` and available < quantity.
   336→
   337→#### Step 2.5: Media Upload
   338→
   339→**File to create:** `App\Jobs\ProcessMediaUpload`
   340→
   341→Requirements:
   342→- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
   343→- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)
   344→
   345→Livewire file upload via `WithFileUploads` trait. Stored on local `public` disk.
   346→
   347→---
   348→
   349→### Phase 3: Themes, Pages, Navigation, Storefront Layout
   350→
   351→**Priority:** HIGH - storefront rendering depends on this.
   352→
   353→#### Step 3.1: Migrations (Batch 3)
   354→
   355→1. `create_themes_table` - FK: `store_id`
   356→2. `create_theme_files_table` - FK: `theme_id`
   357→3. `create_theme_settings_table` - PK: `theme_id`
   358→4. `create_pages_table` - FK: `store_id`
   359→5. `create_navigation_menus_table` - FK: `store_id`
   360→6. `create_navigation_items_table` - FK: `menu_id`
   361→
   362→#### Step 3.2: Models
   363→
   364→**Files to create:**
   365→
   366→| Type | Name | Purpose |
   367→|------|------|---------|
   368→| Model (with migration, factory, seeder) | `Theme` | Store theme entity |
   369→| Model (with migration, factory, seeder) | `ThemeFile` | Individual theme file |
   370→| Model (with migration, factory, seeder) | `ThemeSettings` | Theme configuration |
   371→| Model (with migration, factory, seeder) | `Page` | CMS page |
   372→| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
   373→| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |
   374→
   375→**Relationship definitions:**
   376→
   377→| Model | Key Relationships |
   378→|-------|-------------------|
   379→| `Theme` | `belongsTo(Store)`, `hasMany(ThemeFile)`, `hasOne(ThemeSettings)` |
   380→| `ThemeFile` | `belongsTo(Theme)` |
   381→| `ThemeSettings` | `belongsTo(Theme)` |
   382→| `Page` | `belongsTo(Store)` |
   383→| `NavigationMenu` | `belongsTo(Store)`, `hasMany(NavigationItem)` |
   384→| `NavigationItem` | `belongsTo(NavigationMenu)` |
   385→
   386→**Enums to create:**
   387→
   388→| Enum | Values |
   389→|------|--------|
   390→| `ThemeStatus` | `Draft`, `Published` |
   391→| `PageStatus` | `Draft`, `Published`, `Archived` |
   392→| `NavigationItemType` | `Link`, `Page`, `Collection`, `Product` |
   393→
   394→#### Step 3.3: Storefront Blade Layout
   395→
   396→**Directory structure for `resources/views/storefront/`:**
   397→
   398→| Directory | Files |
   399→|-----------|-------|

content:
#### Step 1.2: Core Migrations (Batch 1-2)

Create in this dependency order:

1. `create_organizations_table` - no dependencies
2. `create_stores_table` - FK: `organization_id`
3. `create_store_domains_table` - FK: `store_id`
4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
6. `create_store_settings_table` - PK: `store_id`

All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.

#### Step 1.3: Core Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Organization` | `hasMany(Store::class)` |
| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
| `StoreDomain` | `belongsTo(Store::class)` |
| `User` | `belongsToMany(Store::class)` through `store_users` |
| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
| `StoreSettings` | `belongsTo(Store::class)` |

Each model must have:
- Explicit return type declarations on all methods
- Proper `$fillable` or `$guarded` arrays
- `casts()` method for JSON/enum columns
- Factory with meaningful default values
- Seeder that creates sample data

#### Step 1.4: Enums

**Files to create in `app/Enums/`:**

| Enum | Values |
|------|--------|
| `StoreStatus` | `Active`, `Suspended` |
| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
| `StoreDomainType` | `Storefront`, `Admin`, `Api` |

#### Step 1.5: Tenant Resolution Middleware

**File to create:** `App\Http\Middleware\ResolveStore`

Requirements:
1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
3. Return 404 if hostname not found
4. Return 503 for suspended stores on storefront routes

**Registration in `bootstrap/app.php`:**
- Register middleware group `storefront` containing `ResolveStore`
- Register middleware group `admin` containing `ResolveStore`

#### Step 1.6: BelongsToStore Trait and Global Scope

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |

Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

#### Step 1.7: Authentication

**Admin auth requirements:**

| Component | Description |
|-----------|-------------|
| Livewire component `Admin\Auth\Login` | Admin login form |
| Livewire component `Admin\Auth\Logout` | Admin logout action |
| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
| Password reset | Standard Laravel flow with `Password::broker('users')` |

**Customer auth requirements:**

| Component | Description |
|-----------|-------------|
| Custom guard | `customer` guard with `customers` provider |
| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
| Uniqueness | Customer email unique per store, not globally |
| Rate limiting | 5 attempts per minute per IP |

**Rate limiter registration:**
- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)

#### Step 1.8: Authorization

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Policy | `ProductPolicy` | Product authorization based on store role |
| Policy | `OrderPolicy` | Order authorization based on store role |
| Policy | `CollectionPolicy` | Collection authorization based on store role |
| Policy | `DiscountPolicy` | Discount authorization based on store role |
| Policy | `CustomerPolicy` | Customer authorization based on store role |
| Policy | `StorePolicy` | Store authorization based on store role |
| Policy | `PagePolicy` | Page authorization based on store role |
| Policy | `ThemePolicy` | Theme authorization based on store role |
| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
| Policy | `RefundPolicy` | Refund authorization based on store role |

Each policy checks user role via `store_users` pivot for the current store. Helper on User model: `roleForStore(Store $store): ?StoreUserRole`. Register policies via auto-discovery or explicit binding in `AppServiceProvider`.

**Permission Matrix:**

| Permission | Owner | Admin | Staff | Support |
|------------|:-----:|:-----:|:-----:|:-------:|
| Manage store settings | Y | Y | N | N |
| Delete store | Y | N | N | N |
| Manage staff | Y | Y | N | N |
| Manage products (CRUD) | Y | Y | Y | N |
| Delete/archive products | Y | Y | N | N |
| Manage orders | Y | Y | Y | Read-only |
| Process refunds | Y | Y | N | N |
| Create fulfillments | Y | Y | Y | N |
| Manage discounts | Y | Y | Y | N |
| Manage themes | Y | Y | N | N |
| Manage apps | Y | Y | N | N |
| View analytics | Y | Y | Y | N |
| Manage shipping/tax | Y | Y | N | N |
| View customers | Y | Y | Y | Y |
| Update customers | Y | Y | Y | N |

---

### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)

**Priority:** HIGH - storefront and orders depend on this.

#### Step 2.1: Migrations (Batch 3-5)

Create in dependency order:

1. `create_products_table` - FK: `store_id`
2. `create_product_options_table` - FK: `product_id`
3. `create_product_option_values_table` - FK: `product_option_id`
4. `create_product_variants_table` - FK: `product_id`
5. `create_variant_option_values_table` - composite PK: `variant_id` + `product_option_value_id`
6. `create_inventory_items_table` - FK: `store_id`, `variant_id`
7. `create_collections_table` - FK: `store_id`
8. `create_collection_products_table` - composite PK: `collection_id` + `product_id`
9. `create_product_media_table` - FK: `product_id`

#### Step 2.2: Models with Relationships

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Product` | Product entity |
| Model (with migration, factory, seeder) | `ProductOption` | Product option (e.g., Size, Color) |
| Model (with migration, factory, seeder) | `ProductOptionValue` | Option values (e.g., S, M, L) |
| Model (with migration, factory, seeder) | `ProductVariant` | Product variant (SKU-level) |
| Model (with migration, factory, seeder) | `InventoryItem` | Inventory tracking per variant |
| Model (with migration, factory, seeder) | `Collection` | Product collection/category |
| Model (with migration, factory, seeder) | `ProductMedia` | Product images and videos |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Product` | `hasMany(ProductVariant)`, `hasMany(ProductOption)`, `hasMany(ProductMedia)`, `belongsToMany(Collection)` via `collection_products` |
| `ProductOption` | `belongsTo(Product)`, `hasMany(ProductOptionValue)` |
| `ProductOptionValue` | `belongsTo(ProductOption)` |
| `ProductVariant` | `belongsTo(Product)`, `hasOne(InventoryItem)`, `belongsToMany(ProductOptionValue)` via `variant_option_values` |
| `InventoryItem` | `belongsTo(ProductVariant)` |
| `Collection` | `belongsToMany(Product)` via `collection_products` |
| `ProductMedia` | `belongsTo(Product)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `ProductStatus` | `Draft`, `Active`, `Archived` |
| `VariantStatus` | `Active`, `Archived` |
| `CollectionStatus` | `Draft`, `Active`, `Archived` |
| `MediaType` | `Image`, `Video` |
| `MediaStatus` | `Processing`, `Ready`, `Failed` |
| `InventoryPolicy` | `Deny`, `Continue` |

#### Step 2.3: Product Service

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

| Method | Description |
|--------|-------------|
| `create(Store $store, array $data): Product` | Creates product with nested variants/options |
| `update(Product $product, array $data): Product` | Updates product |
| `transitionStatus(Product $product, ProductStatus $newStatus): void` | Validates transitions per state machine |
| `delete(Product $product): void` | Hard delete only if draft with no order references; otherwise throws |

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

| Method | Description |
|--------|-------------|
| `rebuildMatrix(Product $product): void` | Computes cartesian product, creates missing variants, archives orphaned ones |

**File to create:** `App\Support\HandleGenerator`

| Method | Description |
|--------|-------------|
| `generate(string $title, string $table, int $storeId, ?int $excludeId = null): string` | Generates unique slugs |

#### Step 2.4: Inventory Service

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

| Method | Description |
|--------|-------------|
| `checkAvailability(InventoryItem $item, int $quantity): bool` | Checks if sufficient available inventory exists |
| `reserve(InventoryItem $item, int $quantity): void` | Increments `quantity_reserved` |
| `release(InventoryItem $item, int $quantity): void` | Decrements `quantity_reserved` |
| `commit(InventoryItem $item, int $quantity): void` | Decrements both `on_hand` and `reserved` |
| `restock(InventoryItem $item, int $quantity): void` | Increments `quantity_on_hand` |

All operations wrapped in database transactions. Throws `InsufficientInventoryException` when policy is `deny` and available < quantity.

#### Step 2.5: Media Upload

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

Requirements:
- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)

Livewire file upload via `WithFileUploads` trait. Stored on local `public` disk.

---

### Phase 3: Themes, Pages, Navigation, Storefront Layout

**Priority:** HIGH - storefront rendering depends on this.

#### Step 3.1: Migrations (Batch 3)

1. `create_themes_table` - FK: `store_id`
2. `create_theme_files_table` - FK: `theme_id`
3. `create_theme_settings_table` - PK: `theme_id`
4. `create_pages_table` - FK: `store_id`
5. `create_navigation_menus_table` - FK: `store_id`
6. `create_navigation_items_table` - FK: `menu_id`

#### Step 3.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Theme` | Store theme entity |
| Model (with migration, factory, seeder) | `ThemeFile` | Individual theme file |
| Model (with migration, factory, seeder) | `ThemeSettings` | Theme configuration |
| Model (with migration, factory, seeder) | `Page` | CMS page |
| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Theme` | `belongsTo(Store)`, `hasMany(ThemeFile)`, `hasOne(ThemeSettings)` |
| `ThemeFile` | `belongsTo(Theme)` |
| `ThemeSettings` | `belongsTo(Theme)` |
| `Page` | `belongsTo(Store)` |
| `NavigationMenu` | `belongsTo(Store)`, `hasMany(NavigationItem)` |
| `NavigationItem` | `belongsTo(NavigationMenu)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `ThemeStatus` | `Draft`, `Published` |
| `PageStatus` | `Draft`, `Published`, `Archived` |
| `NavigationItemType` | `Link`, `Page`, `Collection`, `Product` |

#### Step 3.3: Storefront Blade Layout

**Directory structure for `resources/views/storefront/`:**

| Directory | Files |
|-----------|-------|


content:
   400→| `layouts/` | `app.blade.php` |
   401→| `pages/` | `home.blade.php`, `collection.blade.php`, `product.blade.php`, `cart.blade.php`, `search.blade.php`, `page.blade.php` |
   402→| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
   403→| `account/` | `login.blade.php`, `register.blade.php`, `dashboard.blade.php` |
   404→| `account/orders/` | `index.blade.php`, `show.blade.php` |
   405→| `account/addresses/` | `index.blade.php` |
   406→| `errors/` | `404.blade.php`, `503.blade.php` |
   407→| `components/` | `product-card.blade.php`, `price.blade.php`, `badge.blade.php`, `quantity-selector.blade.php`, `address-form.blade.php`, `order-summary.blade.php`, `breadcrumbs.blade.php`, `pagination.blade.php` |
   408→
   409→Base layout (`app.blade.php`): header with nav, announcement bar, main content, footer, cart drawer. All with dark mode support via `dark:` prefix.
   410→
   411→**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.
   412→
   413→#### Step 3.4: Storefront Livewire Components
   414→
   415→**Files to create:**
   416→
   417→| Component | Purpose |
   418→|-----------|---------|
   419→| `Storefront\Home` | Dynamic sections based on theme settings |
   420→| `Storefront\Collections\Index` | All collections grid |
   421→| `Storefront\Collections\Show` | Product grid with filters/sort/pagination |
   422→| `Storefront\Products\Show` | Variant selection, image gallery, add-to-cart |
   423→| `Storefront\Cart\Show` | Full cart page with line items and totals |
   424→| `Storefront\CartDrawer` | Slide-out cart drawer (always in layout) |
   425→| `Storefront\Search\Index` | Search results with filters |
   426→| `Storefront\Search\Modal` | Search modal with autocomplete |
   427→| `Storefront\Pages\Show` | Static CMS page rendering |
   428→
   429→#### Step 3.5: Navigation Service
   430→
   431→**File to create:** `App\Services\NavigationService`
   432→
   433→| Method | Description |
   434→|--------|-------------|
   435→| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
   436→| `resolveUrl(NavigationItem $item): string` | Resolves URLs for page, collection, product type items |
   437→
   438→Caches per store with 5-minute TTL.
   439→
   440→---
   441→
   442→### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
   443→
   444→**Priority:** HIGH - core shopping flow.
   445→
   446→#### Step 4.1: Migrations (Batch 4-5)
   447→
   448→1. `create_carts_table` - FK: `store_id`, `customer_id`
   449→2. `create_cart_lines_table` - FK: `cart_id`, `variant_id`
   450→3. `create_checkouts_table` - FK: `store_id`, `cart_id`, `customer_id`
   451→4. `create_shipping_zones_table` - FK: `store_id`
   452→5. `create_shipping_rates_table` - FK: `zone_id`
   453→6. `create_tax_settings_table` - PK: `store_id`
   454→7. `create_discounts_table` - FK: `store_id`
   455→
   456→#### Step 4.2: Models
   457→
   458→**Files to create:**
   459→
   460→| Type | Name | Purpose |
   461→|------|------|---------|
   462→| Model (with migration, factory, seeder) | `Cart` | Shopping cart entity |
   463→| Model (with migration, factory, seeder) | `CartLine` | Individual cart line item |
   464→| Model (with migration, factory, seeder) | `Checkout` | Checkout session entity |
   465→| Model (with migration, factory, seeder) | `ShippingZone` | Shipping zone configuration |
   466→| Model (with migration, factory, seeder) | `ShippingRate` | Shipping rate within a zone |
   467→| Model (with migration, factory, seeder) | `TaxSettings` | Per-store tax configuration |
   468→| Model (with migration, factory, seeder) | `Discount` | Discount code or automatic discount |
   469→
   470→**Relationship definitions:**
   471→
   472→| Model | Key Relationships |
   473→|-------|-------------------|
   474→| `Cart` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(CartLine)`, `hasMany(Checkout)` |
   475→| `CartLine` | `belongsTo(Cart)`, `belongsTo(ProductVariant)` |
   476→| `Checkout` | `belongsTo(Store)`, `belongsTo(Cart)`, `belongsTo(Customer)` |
   477→| `ShippingZone` | `belongsTo(Store)`, `hasMany(ShippingRate)` |
   478→| `ShippingRate` | `belongsTo(ShippingZone)` |
   479→| `TaxSettings` | `belongsTo(Store)` |
   480→| `Discount` | `belongsTo(Store)` |
   481→
   482→**Enums to create:**
   483→
   484→| Enum | Values |
   485→|------|--------|
   486→| `CartStatus` | `Active`, `Converted`, `Abandoned` |
   487→| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
   488→| `DiscountType` | `Code`, `Automatic` |
   489→| `DiscountValueType` | `Percent` (`percent`), `Fixed` (`fixed`), `FreeShipping` (`free_shipping`) -- PascalCase cases backed by lowercase string values stored in DB |
   490→| `DiscountStatus` | `Draft`, `Active`, `Expired`, `Disabled` |
   491→| `ShippingRateType` | `Flat`, `Weight`, `Price`, `Carrier` |
   492→| `TaxMode` | `Manual`, `Provider` |
   493→
   494→#### Step 4.3: Cart Service
   495→
   496→**File to create:** `App\Services\CartService`
   497→
   498→| Method | Description |
   499→|--------|-------------|
   500→| `create(Store, ?Customer): Cart` | Creates cart with store currency, version 1 |
   501→| `addLine(Cart, int $variantId, int $qty): CartLine` | Validates variant/product active, inventory, increments existing line or creates new |
   502→| `updateLineQuantity(Cart, int $lineId, int $qty): CartLine` | Validates quantity, recalculates amounts |
   503→| `removeLine(Cart, int $lineId): void` | Deletes line, increments version |
   504→| `getOrCreateForSession(Store, ?Customer): Cart` | Session-based cart binding |
   505→| `mergeOnLogin(Cart $guest, Cart $customer): Cart` | Merges guest cart into customer cart |
   506→
   507→All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.
   508→
   509→#### Step 4.4: Discount Service
   510→
   511→**File to create:** `App\Services\DiscountService`
   512→
   513→| Method | Description |
   514→|--------|-------------|
   515→| `validate(string $code, Store $store, Cart $cart): Discount` | Validates code exists, is active, dates valid, usage limit not reached, minimum purchase met |
   516→| `calculate(Discount $discount, int $subtotal, array $lines): DiscountResult` | Calculates amount, allocates proportionally across qualifying lines |
   517→
   518→Case-insensitive code lookup. Throws `InvalidDiscountException` with specific reason codes.
   519→
   520→#### Step 4.5: Shipping Calculator
   521→
   522→**File to create:** `App\Services\ShippingCalculator`
   523→
   524→| Method | Description |
   525→|--------|-------------|
   526→| `getAvailableRates(Store $store, array $address): Collection` | Matches zones by country/region, returns active rates |
   527→| `calculate(ShippingRate $rate, Cart $cart): int` | Calculates cost based on rate type (flat/weight/price) |
   528→
   529→Zone matching: checks `countries_json` for country code, then `regions_json` for region code.
   530→
   531→#### Step 4.6: Tax Calculator
   532→
   533→**File to create:** `App\Services\TaxCalculator`
   534→
   535→| Method | Description |
   536→|--------|-------------|
   537→| `calculate(int $amount, TaxSettings $settings, array $address): TaxResult` | Calculates tax per manual rate or provider |
   538→| `extractInclusive(int $grossAmount, int $rateBasisPoints): int` | Extracts tax from gross (prices-include-tax mode) |
   539→| `addExclusive(int $netAmount, int $rateBasisPoints): int` | Adds tax to net amount |
   540→
   541→All math uses integers. Rates stored in basis points (1900 = 19%).
   542→
   543→#### Step 4.7: Pricing Engine
   544→
   545→**File to create:** `App\Services\PricingEngine`
   546→
   547→Method: `calculate(Checkout $checkout): PricingResult`
   548→
   549→**Pipeline steps:**
   550→1. Line subtotals: `unit_price * quantity` per line
   551→2. Cart subtotal: sum of line subtotals
   552→3. Discount: apply rules, allocate proportionally
   553→4. Discounted subtotal: subtotal minus discount
   554→5. Shipping: based on selected method/rate
   555→6. Tax: on discounted amounts (plus shipping if taxable)
   556→7. Total: discounted_subtotal + shipping + tax_total
   557→
   558→Result stored in `checkouts.totals_json` as snapshot.
   559→
   560→**Value objects to create:**
   561→
   562→| Type | Name | Fields |
   563→|------|------|--------|
   564→| Value Object | `App\ValueObjects\PricingResult` | subtotal, discount, shipping, taxLines, taxTotal, total, currency |
   565→| Value Object | `App\ValueObjects\TaxLine` | name, rate (basis points), amount (cents) |
   566→
   567→#### Step 4.8: Checkout State Machine
   568→
   569→**File to create:** `App\Services\CheckoutService`
   570→
   571→| Transition | Method | Requirements |
   572→|------------|--------|-------------|
   573→| `started` -> `addressed` | `setAddress(Checkout, array)` | Valid email, shipping address fields |
   574→| `addressed` -> `shipping_selected` | `setShippingMethod(Checkout, int $rateId)` | Rate belongs to applicable zone |
   575→| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
   576→| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
   577→| Any active -> `expired` | `expireCheckout(Checkout)` | Via scheduled job |
   578→
   579→**Files to create:**
   580→
   581→| Type | Name | Purpose |
   582→|------|------|---------|
   583→| Job | `App\Jobs\ExpireAbandonedCheckouts` | Runs every 15 minutes via `routes/console.php`; finds checkouts with `expires_at < now()` and active status; releases reserved inventory, transitions to `expired` |
   584→| Job | `App\Jobs\CleanupAbandonedCarts` | Runs daily; marks carts with `status = active` and `updated_at < 14 days ago` as `abandoned` |
   585→
   586→#### Step 4.9: Storefront Cart/Checkout UI
   587→
   588→**Files to create:**
   589→
   590→| Component | Description |
   591→|-----------|-------------|
   592→| `Storefront\CartDrawer` | Slide-out panel: line items, quantity controls, discount code input, subtotal, checkout button |
   593→| `Storefront\Cart\Show` | Full cart page with same functionality plus shipping estimate |
   594→| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
   595→| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |
   596→
   597→---
   598→
   599→### Phase 5: Payments, Orders, Fulfillment
   600→
   601→**Priority:** HIGH - completes the purchase flow.
   602→
   603→#### Step 5.1: Migrations (Batch 5-7)
   604→
   605→1. `create_customers_table` - FK: `store_id`
   606→2. `create_customer_addresses_table` - FK: `customer_id`
   607→3. `create_orders_table` - FK: `store_id`, `customer_id`
   608→4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
   609→5. `create_payments_table` - FK: `order_id`
   610→6. `create_refunds_table` - FK: `order_id`, `payment_id`
   611→7. `create_fulfillments_table` - FK: `order_id`
   612→8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`
   613→
   614→#### Step 5.2: Models
   615→
   616→**Files to create:**
   617→
   618→| Type | Name | Purpose |
   619→|------|------|---------|
   620→| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
   621→| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
   622→| Model (with migration, factory, seeder) | `Order` | Order entity |
   623→| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
   624→| Model (with migration, factory, seeder) | `Payment` | Payment record |
   625→| Model (with migration, factory, seeder) | `Refund` | Refund record |
   626→| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
   627→| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |
   628→
   629→**Relationship definitions:**
   630→
   631→| Model | Key Relationships |
   632→|-------|-------------------|
   633→| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
   634→| `CustomerAddress` | `belongsTo(Customer)` |
   635→| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
   636→| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
   637→| `Payment` | `belongsTo(Order)` |
   638→| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
   639→| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
   640→| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |
   641→
   642→**Enums to create:**
   643→
   644→| Enum | Values |
   645→|------|--------|
   646→| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
   647→| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
   648→| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
   649→| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
   650→| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
   651→| `RefundStatus` | `Pending`, `Processed`, `Failed` |
   652→| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |
   653→
   654→#### Step 5.3: Payment Service (Mock PSP)
   655→
   656→**Files to create:**
   657→
   658→| Type | Name | Purpose |
   659→|------|------|---------|
   660→| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
   661→| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
   662→| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |
   663→
   664→**PaymentProvider interface methods:**
   665→
   666→| Method | Description |
   667→|--------|-------------|
   668→| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
   669→| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |
   670→
   671→**MockPaymentProvider requirements:**
   672→- No external API calls, no webhooks, no redirects
   673→- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
   674→- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
   675→- Bank transfer: deferred capture, returns PaymentResult with status=pending
   676→- Generates mock reference IDs (`mock_` + random string)
   677→
   678→**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
   679→
   680→#### Step 5.3b: Admin Payment Confirmation
   681→
   682→**Files to create:**
   683→
   684→| Type | Name | Purpose |
   685→|------|------|---------|
   686→| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |
   687→
   688→**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
   689→1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
   690→2. Update `financial_status` to `paid`, `status` to `paid`
   691→3. Update payment record status to `captured`
   692→4. Commit reserved inventory
   693→5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
   694→6. Dispatch `OrderPaid` event
   695→
   696→#### Step 5.4: Order Service
   697→
   698→**File to create:** `App\Services\OrderService`
   699→
   700→| Method | Description |
   701→|--------|-------------|
   702→| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
   703→| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
   704→| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |
   705→
   706→Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.
   707→
   708→#### Step 5.5: Refund Service
   709→
   710→**File to create:** `App\Services\RefundService`
   711→
   712→| Method | Description |
   713→|--------|-------------|
   714→| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |
   715→
   716→- Partial refunds: `financial_status` -> `partially_refunded`
   717→- Full refunds: `financial_status` -> `refunded`
   718→- Restock flag: calls `InventoryService::restock()` for each line
   719→
   720→#### Step 5.6: Fulfillment Service
   721→
   722→**File to create:** `App\Services\FulfillmentService`
   723→
   724→| Method | Description |
   725→|--------|-------------|
   726→| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
   727→| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
   728→| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |
   729→
   730→Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).
   731→
   732→**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).
   733→
   734→**Events to create:**
   735→
   736→| Type | Name | Purpose |
   737→|------|------|---------|
   738→| Event | `OrderCreated` | Dispatched when order is created from checkout |
   739→| Event | `OrderPaid` | Dispatched when payment is captured |
   740→| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
   741→| Event | `OrderCancelled` | Dispatched when order is cancelled |
   742→| Event | `OrderRefunded` | Dispatched when refund is processed |
   743→
   744→---
   745→
   746→### Phase 6: Customer Accounts
   747→
   748→**Priority:** MEDIUM - enhances the shopping experience.
   749→
   750→#### Step 6.1: Customer Auth
   751→
   752→- `Customer` model with custom `customer` guard (already configured in Phase 1)
   753→- Custom `CustomerUserProvider` - scopes queries by `store_id`
   754→- Customer email unique per store, not globally
   755→
   756→#### Step 6.2: Customer Account Pages (Livewire)
   757→
   758→**Files to create:**
   759→
   760→| Component | Route | Description |
   761→|-----------|-------|-------------|
   762→| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
   763→| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
   764→| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
   765→| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
   766→| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
   767→| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |
   768→
   769→Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
   770→
   771→---
   772→
   773→### Phase 7: Admin Panel
   774→
   775→**Priority:** MEDIUM - merchant management interface.
   776→
   777→#### Step 7.1: Admin Layout
   778→
   779→**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
   780→
   781→**Components to create:**
   782→
   783→| Component | Description |
   784→|-----------|-------------|
   785→| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
   786→| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
   787→| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |
   788→
   789→Toast notifications via Livewire events.
   790→
   791→#### Step 7.2: Dashboard
   792→
   793→**File to create:** `Admin\Dashboard` Livewire component
   794→
   795→Requirements:
   796→- KPI tiles: total sales (period), order count, average order value, conversion rate
   797→- Orders chart (last 30 days line chart)
   798→- Recent orders table (last 10)
   799→- Date range filtering via Livewire properties

content:
| `layouts/` | `app.blade.php` |
| `pages/` | `home.blade.php`, `collection.blade.php`, `product.blade.php`, `cart.blade.php`, `search.blade.php`, `page.blade.php` |
| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
| `account/` | `login.blade.php`, `register.blade.php`, `dashboard.blade.php` |
| `account/orders/` | `index.blade.php`, `show.blade.php` |
| `account/addresses/` | `index.blade.php` |
| `errors/` | `404.blade.php`, `503.blade.php` |
| `components/` | `product-card.blade.php`, `price.blade.php`, `badge.blade.php`, `quantity-selector.blade.php`, `address-form.blade.php`, `order-summary.blade.php`, `breadcrumbs.blade.php`, `pagination.blade.php` |

Base layout (`app.blade.php`): header with nav, announcement bar, main content, footer, cart drawer. All with dark mode support via `dark:` prefix.

**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.

#### Step 3.4: Storefront Livewire Components

**Files to create:**

| Component | Purpose |
|-----------|---------|
| `Storefront\Home` | Dynamic sections based on theme settings |
| `Storefront\Collections\Index` | All collections grid |
| `Storefront\Collections\Show` | Product grid with filters/sort/pagination |
| `Storefront\Products\Show` | Variant selection, image gallery, add-to-cart |
| `Storefront\Cart\Show` | Full cart page with line items and totals |
| `Storefront\CartDrawer` | Slide-out cart drawer (always in layout) |
| `Storefront\Search\Index` | Search results with filters |
| `Storefront\Search\Modal` | Search modal with autocomplete |
| `Storefront\Pages\Show` | Static CMS page rendering |

#### Step 3.5: Navigation Service

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

| Method | Description |
|--------|-------------|
| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
| `resolveUrl(NavigationItem $item): string` | Resolves URLs for page, collection, product type items |

Caches per store with 5-minute TTL.

---

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes

**Priority:** HIGH - core shopping flow.

#### Step 4.1: Migrations (Batch 4-5)

1. `create_carts_table` - FK: `store_id`, `customer_id`
2. `create_cart_lines_table` - FK: `cart_id`, `variant_id`
3. `create_checkouts_table` - FK: `store_id`, `cart_id`, `customer_id`
4. `create_shipping_zones_table` - FK: `store_id`
5. `create_shipping_rates_table` - FK: `zone_id`
6. `create_tax_settings_table` - PK: `store_id`
7. `create_discounts_table` - FK: `store_id`

#### Step 4.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Cart` | Shopping cart entity |
| Model (with migration, factory, seeder) | `CartLine` | Individual cart line item |
| Model (with migration, factory, seeder) | `Checkout` | Checkout session entity |
| Model (with migration, factory, seeder) | `ShippingZone` | Shipping zone configuration |
| Model (with migration, factory, seeder) | `ShippingRate` | Shipping rate within a zone |
| Model (with migration, factory, seeder) | `TaxSettings` | Per-store tax configuration |
| Model (with migration, factory, seeder) | `Discount` | Discount code or automatic discount |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Cart` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(CartLine)`, `hasMany(Checkout)` |
| `CartLine` | `belongsTo(Cart)`, `belongsTo(ProductVariant)` |
| `Checkout` | `belongsTo(Store)`, `belongsTo(Cart)`, `belongsTo(Customer)` |
| `ShippingZone` | `belongsTo(Store)`, `hasMany(ShippingRate)` |
| `ShippingRate` | `belongsTo(ShippingZone)` |
| `TaxSettings` | `belongsTo(Store)` |
| `Discount` | `belongsTo(Store)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `CartStatus` | `Active`, `Converted`, `Abandoned` |
| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
| `DiscountType` | `Code`, `Automatic` |
| `DiscountValueType` | `Percent` (`percent`), `Fixed` (`fixed`), `FreeShipping` (`free_shipping`) -- PascalCase cases backed by lowercase string values stored in DB |
| `DiscountStatus` | `Draft`, `Active`, `Expired`, `Disabled` |
| `ShippingRateType` | `Flat`, `Weight`, `Price`, `Carrier` |
| `TaxMode` | `Manual`, `Provider` |

#### Step 4.3: Cart Service

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

| Method | Description |
|--------|-------------|
| `create(Store, ?Customer): Cart` | Creates cart with store currency, version 1 |
| `addLine(Cart, int $variantId, int $qty): CartLine` | Validates variant/product active, inventory, increments existing line or creates new |
| `updateLineQuantity(Cart, int $lineId, int $qty): CartLine` | Validates quantity, recalculates amounts |
| `removeLine(Cart, int $lineId): void` | Deletes line, increments version |
| `getOrCreateForSession(Store, ?Customer): Cart` | Session-based cart binding |
| `mergeOnLogin(Cart $guest, Cart $customer): Cart` | Merges guest cart into customer cart |

All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.

#### Step 4.4: Discount Service

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

| Method | Description |
|--------|-------------|
| `validate(string $code, Store $store, Cart $cart): Discount` | Validates code exists, is active, dates valid, usage limit not reached, minimum purchase met |
| `calculate(Discount $discount, int $subtotal, array $lines): DiscountResult` | Calculates amount, allocates proportionally across qualifying lines |

Case-insensitive code lookup. Throws `InvalidDiscountException` with specific reason codes.

#### Step 4.5: Shipping Calculator

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

| Method | Description |
|--------|-------------|
| `getAvailableRates(Store $store, array $address): Collection` | Matches zones by country/region, returns active rates |
| `calculate(ShippingRate $rate, Cart $cart): int` | Calculates cost based on rate type (flat/weight/price) |

Zone matching: checks `countries_json` for country code, then `regions_json` for region code.

#### Step 4.6: Tax Calculator

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

| Method | Description |
|--------|-------------|
| `calculate(int $amount, TaxSettings $settings, array $address): TaxResult` | Calculates tax per manual rate or provider |
| `extractInclusive(int $grossAmount, int $rateBasisPoints): int` | Extracts tax from gross (prices-include-tax mode) |
| `addExclusive(int $netAmount, int $rateBasisPoints): int` | Adds tax to net amount |

All math uses integers. Rates stored in basis points (1900 = 19%).

#### Step 4.7: Pricing Engine

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

Method: `calculate(Checkout $checkout): PricingResult`

**Pipeline steps:**
1. Line subtotals: `unit_price * quantity` per line
2. Cart subtotal: sum of line subtotals
3. Discount: apply rules, allocate proportionally
4. Discounted subtotal: subtotal minus discount
5. Shipping: based on selected method/rate
6. Tax: on discounted amounts (plus shipping if taxable)
7. Total: discounted_subtotal + shipping + tax_total

Result stored in `checkouts.totals_json` as snapshot.

**Value objects to create:**

| Type | Name | Fields |
|------|------|--------|
| Value Object | `App\ValueObjects\PricingResult` | subtotal, discount, shipping, taxLines, taxTotal, total, currency |
| Value Object | `App\ValueObjects\TaxLine` | name, rate (basis points), amount (cents) |

#### Step 4.8: Checkout State Machine

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

| Transition | Method | Requirements |
|------------|--------|-------------|
| `started` -> `addressed` | `setAddress(Checkout, array)` | Valid email, shipping address fields |
| `addressed` -> `shipping_selected` | `setShippingMethod(Checkout, int $rateId)` | Rate belongs to applicable zone |
| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
| Any active -> `expired` | `expireCheckout(Checkout)` | Via scheduled job |

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\ExpireAbandonedCheckouts` | Runs every 15 minutes via `routes/console.php`; finds checkouts with `expires_at < now()` and active status; releases reserved inventory, transitions to `expired` |
| Job | `App\Jobs\CleanupAbandonedCarts` | Runs daily; marks carts with `status = active` and `updated_at < 14 days ago` as `abandoned` |

#### Step 4.9: Storefront Cart/Checkout UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\CartDrawer` | Slide-out panel: line items, quantity controls, discount code input, subtotal, checkout button |
| `Storefront\Cart\Show` | Full cart page with same functionality plus shipping estimate |
| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |

---

### Phase 5: Payments, Orders, Fulfillment

**Priority:** HIGH - completes the purchase flow.

#### Step 5.1: Migrations (Batch 5-7)

1. `create_customers_table` - FK: `store_id`
2. `create_customer_addresses_table` - FK: `customer_id`
3. `create_orders_table` - FK: `store_id`, `customer_id`
4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
5. `create_payments_table` - FK: `order_id`
6. `create_refunds_table` - FK: `order_id`, `payment_id`
7. `create_fulfillments_table` - FK: `order_id`
8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`

#### Step 5.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
| Model (with migration, factory, seeder) | `Order` | Order entity |
| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
| Model (with migration, factory, seeder) | `Payment` | Payment record |
| Model (with migration, factory, seeder) | `Refund` | Refund record |
| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
| `CustomerAddress` | `belongsTo(Customer)` |
| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
| `Payment` | `belongsTo(Order)` |
| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
| `RefundStatus` | `Pending`, `Processed`, `Failed` |
| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |

#### Step 5.3: Payment Service (Mock PSP)

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |

**PaymentProvider interface methods:**

| Method | Description |
|--------|-------------|
| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |

**MockPaymentProvider requirements:**
- No external API calls, no webhooks, no redirects
- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
- Bank transfer: deferred capture, returns PaymentResult with status=pending
- Generates mock reference IDs (`mock_` + random string)

**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.

#### Step 5.3b: Admin Payment Confirmation

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |

**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
2. Update `financial_status` to `paid`, `status` to `paid`
3. Update payment record status to `captured`
4. Commit reserved inventory
5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
6. Dispatch `OrderPaid` event

#### Step 5.4: Order Service

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

| Method | Description |
|--------|-------------|
| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |

Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.

#### Step 5.5: Refund Service

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

| Method | Description |
|--------|-------------|
| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |

- Partial refunds: `financial_status` -> `partially_refunded`
- Full refunds: `financial_status` -> `refunded`
- Restock flag: calls `InventoryService::restock()` for each line

#### Step 5.6: Fulfillment Service

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

| Method | Description |
|--------|-------------|
| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |

Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).

**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).

**Events to create:**

| Type | Name | Purpose |
|------|------|---------|
| Event | `OrderCreated` | Dispatched when order is created from checkout |
| Event | `OrderPaid` | Dispatched when payment is captured |
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |
|-----------|-------|-------------|
| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |

Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.

---

### Phase 7: Admin Panel

**Priority:** MEDIUM - merchant management interface.

#### Step 7.1: Admin Layout

**File to create:** `resources/views/livewire/admin/layout/app.blade.php`

**Components to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |

Toast notifications via Livewire events.

#### Step 7.2: Dashboard

**File to create:** `Admin\Dashboard` Livewire component

Requirements:
- KPI tiles: total sales (period), order count, average order value, conversion rate
- Orders chart (last 30 days line chart)
- Recent orders table (last 10)
- Date range filtering via Livewire properties