sed -n '1,340p' specs/05-BUSINESS-LOGIC.md

# 05 - Business Logic Specification

> E-commerce platform - All domain logic, state machines, calculations, and rules for Laravel 12

---

## Conventions

| Convention | Detail |
|---|---|
| Monetary amounts | **INTEGER in minor units** (cents). All arithmetic uses integers - never floating point. |
| Percentage storage | Basis points where noted (e.g., 1900 = 19.00%) or whole-number percentage in `discounts.value_amount` (e.g., 15 = 15%) |
| Rounding | Round each line independently, then sum (prevents rounding drift) |
| Database | SQLite - single-writer, WAL mode, no advisory locks needed |
| Time | All timestamps in ISO-8601 UTC via Laravel `created_at` / `updated_at` |
| Currency | Single currency per store (`stores.default_currency`). No multi-currency conversion. |

---

## 1. Multi-Tenancy and Store Isolation

### 1.1 Tenant Resolution Middleware

**Class:** `App\Http\Middleware\ResolveStore`

**Register** in `bootstrap/app.php` under both the `storefront` and `admin` middleware groups.

#### Storefront Route Resolution (from request hostname)

1. Extract hostname from the incoming HTTP request.
2. Look up `store_domains` table where `hostname` matches.
3. Cache the hostname-to-store_id mapping using file cache with a 5-minute TTL. Cache key pattern: `store_domain:{hostname}`.
4. Retrieve the full Store model and bind it as a singleton in the application container under the key `current_store`.
5. If hostname is not found in `store_domains`: return HTTP 404.
6. If the resolved store has `status = 'suspended'`: return HTTP 503 maintenance page.

#### Admin Route Resolution (from session)

1. Read the selected store ID from the authenticated user's session (key: `current_store_id`).
2. Verify the authenticated user has a `store_users` record for that store.
3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.

```mermaid
flowchart TD
    A[Incoming Request] --> B{Route type?}
    B -->|Storefront| C[Extract hostname]
    C --> D{Hostname in cache?}
    D -->|Yes| E[Load store from cache]
    D -->|No| F[Query store_domains table]
    F --> G{Found?}
    G -->|No| H[Return 404]
    G -->|Yes| I[Cache hostname->store_id, 5 min TTL]
    I --> E
    E --> J{Store suspended?}
    J -->|Yes| K[Return 503 maintenance]
    J -->|No| L[Bind store as singleton, continue]

    B -->|Admin| M[Read store_id from session]
    M --> N{User has store_users record?}
    N -->|No| O[Return 403]
    N -->|Yes| P{Store suspended?}
    P -->|Yes| Q[Return 403 for mutations]
    P -->|No| L
```

### 1.2 Store Scoping

**Trait:** `App\Models\Concerns\BelongsToStore`
**Global Scope:** `App\Models\Scopes\StoreScope`

The trait applies a global query scope to every model that uses it. The scope adds a `WHERE store_id = <current_store_id>` clause to all queries automatically.

**Behavior:**

- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
- All SELECT, UPDATE, DELETE queries on tenant-scoped models include the store filter.
- In platform-admin contexts where cross-store access is needed, the scope can be temporarily removed.

**Models that must use this trait:**
products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

### 1.3 Role-Based Access Control

**Enum:** `App\Enums\StoreUserRole` - backed by string values: `owner`, `admin`, `staff`, `support`

**Permission Matrix:**

| Permission | Owner | Admin | Staff | Support |
|---|---|---|---|---|
| Manage store settings | Yes | Yes | No | No |
| Manage staff | Yes | Yes | No | No |
| Manage products | Yes | Yes | Yes | No |
| Manage orders | Yes | Yes | Yes | Yes (read-only) |
| Manage discounts | Yes | Yes | Yes | No |
| Manage themes | Yes | Yes | No | No |
| Manage apps | Yes | Yes | No | No |
| View analytics | Yes | Yes | Yes | No |
| Manage shipping/tax | Yes | Yes | No | No |
| Process refunds | Yes | Yes | No | No |
| Create fulfillments | Yes | Yes | Yes | No |

**Implementation requirements:**

- Create a Laravel policy for each resource model (e.g., `ProductPolicy`, `OrderPolicy`).
- Register gate definitions in a service provider.
- Each policy method receives the User, looks up the user's role for the current store via the `store_users` pivot table, and checks the matrix above.
- Add a helper method on the User model - `roleForStore(store)` - that returns the user's role for a given store or null if the user has no access.

---

## 2. Product Lifecycle

### 2.1 Status Transitions

**Enum:** `App\Enums\ProductStatus` - values: `draft`, `active`, `archived`

```mermaid
stateDiagram-v2
    [*] --> Draft : Product created
    Draft --> Active : Publish (requires variant with price > 0, title not empty)
    Draft --> Archived : Archive
    Active --> Archived : Arc…3738 chars truncated…ts held for active checkouts or pending orders |
| **Available** (computed) | `quantity_on_hand - quantity_reserved` |

### 3.2 Inventory Policy

Per-variant policy stored in `inventory_items.policy`:

| Policy | Behavior |
|---|---|
| `deny` | Block add-to-cart when available <= 0. Storefront displays "Out of stock". |
| `continue` | Allow add-to-cart regardless of stock level. When available <= 0, storefront displays "Available on backorder". |

### 3.3 Inventory Operations

**Service:** `App\Services\InventoryService`

All operations must execute within a database transaction.

```mermaid
flowchart LR
    subgraph "Stock Lifecycle"
        A[On Hand] -->|Reserve| B[Reserved]
        B -->|Commit on payment| C[Deducted]
        B -->|Release on expiry| A
        C -->|Restock on refund| A
    end
```

**Operation definitions:**

| Operation | Trigger | Effect on `quantity_on_hand` | Effect on `quantity_reserved` | Pre-check |
|---|---|---|---|---|
| **Reserve** | Checkout enters `payment_selected` | No change | +quantity | If policy=deny, verify available >= quantity. Throw `InsufficientInventoryException` if not. |
| **Release** | Checkout expires or is abandoned | No change | -quantity | None |
| **Commit** | Payment confirmed, order created | -quantity | -quantity | None (reservation already validated) |
| **Restock** | Refund processed with restock flag | +quantity | No change | None |

### 3.4 Concurrency

SQLite operates in single-writer mode with WAL journaling. Database transactions provide sufficient isolation. No optimistic locking or advisory locks are needed.

---

## 4. Cart Service

### 4.1 Cart Lifecycle

**Service:** `App\Services\CartService`

```mermaid
stateDiagram-v2
    [*] --> active : Cart created
    active --> converted : Checkout completed successfully
    active --> abandoned : Cleanup job (14 days inactive)
```

**Cart identification:**
- Guest users: cart ID stored in session cookie.
- Authenticated customers: cart linked via `customer_id` foreign key.

The guest cart is bound to the session using the key `cart_id`. When a guest visits the storefront, a cart is created on first add-to-cart and the cart's ID is stored in `session('cart_id')`. On customer login, if a session cart exists, it is merged with any existing customer cart (the session cart's items are added to the customer cart, preferring the higher quantity for duplicate variants). After merge, the session key is cleared.

**Cart merge on login:**

```
PROCEDURE mergeGuestCart(guestCart, customerCart):
    FOR EACH line IN guestCart.lines:
        existingLine = customerCart.findLineByVariant(line.variant_id)
        IF existingLine EXISTS:
            existingLine.quantity = MAX(existingLine.quantity, line.quantity)
            RECALCULATE existingLine amounts
        ELSE:
            MOVE line to customerCart
    MARK guestCart as abandoned
    RECALCULATE all line amounts on customerCart
```

### 4.2 Cart Operations

**Create cart:**
- Accepts: store, optional customer
- Sets: store_id, currency from store's default_currency, initial cart_version = 1

**Add line - validations (in order):**

1. Variant exists and belongs to the current store (through its product).
2. Product has status = `active`.
3. Variant has status = `active`.
4. Inventory policy check: if policy = `deny`, verify available >= requested quantity.
5. If the variant already exists in the cart, increment existing line quantity instead of creating a duplicate.
6. Set `unit_price_amount` from the variant's `price_amount`.
7. Calculate: `line_subtotal_amount = unit_price_amount * quantity`.
8. Set `line_discount_amount = 0` (recalculated by discount engine during checkout).
9. Calculate: `line_total_amount = line_subtotal_amount - line_discount_amount`.
10. Increment `cart_version`.

**Update quantity:**
- Validate new quantity > 0. If 0, remove the line instead.
- Re-check inventory policy for the new quantity.
- Recalculate line amounts.
- Increment `cart_version`.

**Remove line:**
- Delete the `cart_lines` record.
- Increment `cart_version`.

### 4.3 Cart Version (Optimistic Concurrency)

Every cart mutation increments `cart_version` by 1.

**For API clients:**
- Client sends `expected_version` with each mutation request.
- If cart's current version does not match the expected version, return HTTP 409 Conflict with the current cart state in the response body.
- This prevents race conditions when multiple browser tabs or devices modify the same cart.

**For Livewire/session-based flows:**
- Still implemented for consistency, but less critical since requests are serialized per session.

### 4.4 Line Amount Calculation

```
line_subtotal_amount = unit_price_amount * quantity
line_discount_amount = 0  (updated by discount engine during checkout)
line_total_amount    = line_subtotal_amount - line_discount_amount
```

All values are integers in minor currency units.

### 4.5 Cart Expiration

**Job:** `App\Jobs\CleanupAbandonedCarts`

- Schedule: daily


sed -n '1,320p' specs/04-STOREFRONT-UI.md

# 04 - Storefront UI/UX Specification

> E-commerce platform - Customer-facing storefront UI/UX requirements

---

## Conventions

| Convention | Detail |
|---|---|
| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
| Templating | Blade templates with reusable storefront-prefixed components |
| Accessibility | WCAG 2.2 AA compliance throughout |
| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |

---

## Currency Formatting

All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:

| Setting | Value |
|---------|-------|
| Decimal separator | `.` (period) |
| Thousands separator | `,` (comma) |
| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
| Decimal places | Always 2 |
| Free amounts | Display as `0.00 EUR` |
| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |

**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.

**Examples:**
- 2499 cents -> `24.99 EUR`
- 0 cents -> `0.00 EUR`
- 149900 cents -> `1,499.00 EUR`

---

## 1. Theme System Architecture

### 1.1 View Organization

The storefront views are organized into the following categories:

| Category | Purpose | Views |
|---|---|---|
| Layouts | Master page shell | app (master layout) |
| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
| Checkout | Purchase flow | checkout index, order confirmation |
| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
| Errors | Error states | 404, 503 |
| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |

### 1.2 Layout Hierarchy

All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.

### 1.3 Theme Settings

Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:

- Announcement bar visibility, text, link, and background color
- Sticky header toggle
- Home page section order and visibility
- Color palette overrides (primary, secondary, accent)
- Typography scale
- Footer content and social links
- Dark mode behavior (system preference, toggle, or forced)

### 1.4 Section-Based Architecture

Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.

| Section | Description | Used On |
|---|---|---|
| Hero | Full-width banner with heading, subheading, call-to-action | Home |
| Featured Collections | Grid of collection cards | Home |
| Featured Products | Grid of product cards (lazy-loaded) | Home |
| Newsletter | Email signup form | Home, Footer |
| Rich Text | Customizable content block | Home, Pages |

### 1.5 Blade Components

All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.

---

## 2. Base Layout

### 2.1 Document Head

The document head includes:

| Element | Description |
|---|---|
| Character encoding | UTF-8 |
| Viewport | Width = device width, initial scale 1 |
| Meta description | Page-specific, yielded from child templates |
| Title | Page-specific title appended with the app name |
| Stylesheets | Compiled CSS via Vite |
| Scripts | Compiled JS via Vite |
| Livewire styles | Injected by Livewire |
| Language attribute | Set on the HTML element from application locale |
| Scroll behavior | Smooth scrolling enabled globally |

### 2.2 Skip Link

A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.

**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.

### 2.3 Announcement Bar

Optional, controlled by theme settings. Renders a full-width bar above the header.

| Attribute | Description |
|---|---|
| Position | Full-width, above the header |
| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
| Text | Centered, small size, configurable content |
| Link | Optional, underlined, opens in same tab |
| Dismiss | Close button (X icon) on the right side; dismis…3336 chars truncated… | Row of icon links (Facebook, Instagram, Twitter/X, TikTok, YouTube) configured in theme settings. Each with screen reader label. Icons are 20x20, muted color with hover to full contrast |
| Copyright line | Small muted text: "(c) {year} {store name}. All rights reserved." |
| Payment method icons | Row of SVG icons for accepted payment methods (Visa, Mastercard, Amex, PayPal, etc.). Slightly reduced opacity |
| Spacing | Generous vertical padding, content constrained to max-width container with horizontal padding |
| Background | Light gray (dark: very dark gray), top border separator |

### 2.7 Cart Drawer (Livewire)

The cart drawer is always present in the layout as a global Livewire component. It listens for the "cart-updated" browser event to refresh and open. See Section 6 for full specification.

### 2.8 Dark Mode

Dark mode is implemented via Tailwind's dark variant. The behavior is controlled by theme settings:

| Mode | Implementation |
|---|---|
| System preference (default) | Follows OS-level dark mode preference automatically |
| Toggle | User-controlled toggle button in header/footer; preference stored in localStorage |
| Forced light/dark | Applied at server render time on the HTML element |

**Color Token Patterns:**

| Token | Light Mode | Dark Mode |
|---|---|---|
| Page background | White | Near-black |
| Card/section background | Light gray | Very dark gray |
| Heading text | Near-black | White |
| Body text | Medium gray | Light gray |
| Borders | Light gray | Medium-dark gray |
| Primary actions | Blue, darkens on hover | Slightly lighter blue, lightens on hover |

---

## 3. Home Page (GET /)

The home page renders sections in the order defined by theme settings. Each section can be toggled on/off and reordered. Default section order:

1. Hero Banner
2. Featured Collections
3. Featured Products
4. Newsletter Signup
5. Rich Text

### 3.1 Hero Banner Section

**Layout:** Full-width container (no max-width on the outer wrapper). Inner content constrained to standard max-width.

**Desktop (large breakpoint and above):**

```
+--------------------------------------------------------------+
|                                                               |
|  [Background Image / Gradient]                                |
|                                                               |
|       [Heading - very large, bold]                            |
|       [Subheading - medium, regular weight]                   |
|       [CTA Button - primary, large]                           |
|                                                               |
+--------------------------------------------------------------+
```

| Attribute | Description |
|---|---|
| Background | Full-width image (cover, centered) or gradient. Image set via theme settings. Semi-transparent dark overlay for text readability |
| Content | Centered vertically and horizontally, white text |
| Heading | H1 or H2 depending on context. Very large on desktop, progressively smaller on mobile. Bold, tight tracking |
| Subheading | Regular paragraph, medium size, generous top margin. Constrained to a comfortable reading width |
| CTA | Styled as primary button, large. Links to a configured collection or product |
| Height | Minimum 600px on desktop, 500px on tablet, 400px on mobile, 300px on small mobile |
| Padding | Generous vertical and horizontal padding |

**Mobile:** Same structure but stacked, with reduced font sizes and shorter minimum height. Full-bleed background image remains.

**Accessibility:**

- If background is an image, it is decorative (the content provides meaning)
- CTA button has descriptive text (e.g., "Shop the summer collection")

### 3.2 Featured Collections Section

**Layout:** Section with heading and grid of collection cards.

| Attribute | Description |
|---|---|
| Section heading | H2, large, bold, centered, with generous bottom margin |
| Grid | 2 columns on mobile, 4 columns on large screens, with gap spacing |
| Count | 2 to 4 collections, configurable in theme settings |

**Collection Card:**

```
+---------------------------+
|  [Collection Image]       |
|  3:4 aspect ratio         |
|  cover-fit, rounded       |
|                           |
|  [Title overlay at bottom]|
|  [Shop now ->]            |
+---------------------------+
```

| Element | Description |
|---|---|
| Image | 3:4 aspect ratio, cover-fit, rounded corners. Gradient overlay at bottom fading from transparent to semi-transparent dark |
| Title | White text overlaid at bottom of image, semi-bold, medium-large size |
| "Shop now" link | Small white text below title, slightly transparent, underlined. Becomes fully opaque on hover |
| Interaction | Entire card is clickable. Subtle scale-up on hover with smooth transition |

**Accessibility:**

- Each card link has the collection title as accessible name
- Images have alt text matching the collection title

### 3.3 Featured Products Section

**Livewire Component:** FeaturedProducts


Output:
Chunk ID: e39449
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3334
Output:
# 04 - Storefront UI/UX Specification

> E-commerce platform - Customer-facing storefront UI/UX requirements

---

## Conventions

| Convention | Detail |
|---|---|
| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
| Templating | Blade templates with reusable storefront-prefixed components |
| Accessibility | WCAG 2.2 AA compliance throughout |
| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |

---

## Currency Formatting

All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:

| Setting | Value |
|---------|-------|
| Decimal separator | `.` (period) |
| Thousands separator | `,` (comma) |
| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
| Decimal places | Always 2 |
| Free amounts | Display as `0.00 EUR` |
| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |

**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.

**Examples:**
- 2499 cents -> `24.99 EUR`
- 0 cents -> `0.00 EUR`
- 149900 cents -> `1,499.00 EUR`

---

## 1. Theme System Architecture

### 1.1 View Organization

The storefront views are organized into the following categories:

| Category | Purpose | Views |
|---|---|---|
| Layouts | Master page shell | app (master layout) |
| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
| Checkout | Purchase flow | checkout index, order confirmation |
| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
| Errors | Error states | 404, 503 |
| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |

### 1.2 Layout Hierarchy

All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.

### 1.3 Theme Settings

Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:

- Announcement bar visibility, text, link, and background color
- Sticky header toggle
- Home page section order and visibility
- Color palette overrides (primary, secondary, accent)
- Typography scale
- Footer content and social links
- Dark mode behavior (system preference, toggle, or forced)

### 1.4 Section-Based Architecture

Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.

| Section | Description | Used On |
|---|---|---|
| Hero | Full-width banner with heading, subheading, call-to-action | Home |
| Featured Collections | Grid of collection cards | Home |
| Featured Products | Grid of product cards (lazy-loaded) | Home |
| Newsletter | Email signup form | Home, Footer |
| Rich Text | Customizable content block | Home, Pages |

### 1.5 Blade Components

All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.

---

## 2. Base Layout

### 2.1 Document Head

The document head includes:

| Element | Description |
|---|---|
| Character encoding | UTF-8 |
| Viewport | Width = device width, initial scale 1 |
| Meta description | Page-specific, yielded from child templates |
| Title | Page-specific title appended with the app name |
| Stylesheets | Compiled CSS via Vite |
| Scripts | Compiled JS via Vite |
| Livewire styles | Injected by Livewire |
| Language attribute | Set on the HTML element from application locale |
| Scroll behavior | Smooth scrolling enabled globally |

### 2.2 Skip Link

A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.

**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.

### 2.3 Announcement Bar

Optional, controlled by theme settings. Renders a full-width bar above the header.

| Attribute | Description |
|---|---|
| Position | Full-width, above the header |
| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
| Text | Centered, small size, configurable content |
| Link | Optional, underlined, opens in same tab |
| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
| Height | Auto height with vertical padding |

### 2.4 Header

The header serves as the primary navigation banner.

**Desktop Layout (large breakpoint and above):**

```
+--------------------------------------------------------------+
| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
| Search Icon | Right group | Button that opens the search modal |
| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |

**Mobile Layout (below large breakpoint):**

```
+--------------------------------------------------------------+
| [Hamburger]           [Logo]                            [Cart]|
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Hamburger menu | Left | Toggle button for mobile navigation drawer |
| Logo | Center | Same as desktop but horizontally centered |
| Cart icon | Right | Same as desktop |

**Sticky Header (optional):**

When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.

**Mobile Navigation Drawer:**

| Attribute | Description |
|---|---|
| Animation | Slides in from the left side |
| Height | Full viewport height |
| Backdrop | Semi-transparent dark overlay behind the drawer |
| Close button | X icon at the top-right of the drawer |
| Navigation items | Vertical stack with comfortable tap targets |
| Submenus | Collapsible accordion pattern with chevron icons |
| Account link | Positioned at the bottom of the navigation list |
| Focus behavior | Focus is trapped within the drawer while open |
| ARIA | Labeled as "Mobile navigation" |

### 2.5 Main Content Area

The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.

### 2.6 Footer

The footer is a content-info landmark.

**Layout:**

```
+--------------------------------------------------------------+
| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
|                                                               |
| [Social Icons]                                                |
| [Copyright] [Payment Icons]                                   |
+--------------------------------------------------------------+
```

| Element | Description |
|---|---|
| Link columns | Responsive grid: 2 columns on mobile, 3 on tablet, up to 4 on desktop. Links sourced from "footer-menu" navigation menu. Each column has an uppercase heading and a list of links |
| Store info column | Store name, address (if configured), contact email |
| Social links | Row of icon links (Facebook, Instagram, Twitter/X, TikTok, YouTube) configured in theme settings. Each with screen reader label. Icons are 20x20, muted color with hover to full contrast |
| Copyright line | Small muted text: "(c) {year} {store name}. All rights reserved." |
| Payment method icons | Row of SVG icons for accepted payment methods (Visa, Mastercard, Amex, PayPal, etc.). Slightly reduced opacity |
| Spacing | Generous vertical padding, content constrained to max-width container with horizontal padding |
| Background | Light gray (dark: very dark gray), top border separator |

### 2.7 Cart Drawer (Livewire)

The cart drawer is always present in the layout as a global Livewire component. It listens for the "cart-updated" browser event to refresh and open. See Section 6 for full specification.

### 2.8 Dark Mode

Dark mode is implemented via Tailwind's dark variant. The behavior is controlled by theme settings:

| Mode | Implementation |
|---|---|
| System preference (default) | Follows OS-level dark mode preference automatically |
| Toggle | User-controlled toggle button in header/footer; preference stored in localStorage |
| Forced light/dark | Applied at server render time on the HTML element |

**Color Token Patterns:**

| Token | Light Mode | Dark Mode |
|---|---|---|
| Page background | White | Near-black |
| Card/section background | Light gray | Very dark gray |
| Heading text | Near-black | White |
| Body text | Medium gray | Light gray |
| Borders | Light gray | Medium-dark gray |
| Primary actions | Blue, darkens on hover | Slightly lighter blue, lightens on hover |

---

## 3. Home Page (GET /)

The home page renders sections in the order defined by theme settings. Each section can be toggled on/off and reordered. Default section order:

1. Hero Banner
2. Featured Collections
3. Featured Products
4. Newsletter Signup
5. Rich Text

### 3.1 Hero Banner Section

**Layout:** Full-width container (no max-width on the outer wrapper). Inner content constrained to standard max-width.

**Desktop (large breakpoint and above):**

```
+--------------------------------------------------------------+
|                                                               |
|  [Background Image / Gradient]                                |
|                                                               |
|       [Heading - very large, bold]                            |
|       [Subheading - medium, regular weight]                   |
|       [CTA Button - primary, large]                           |
|                                                               |
+--------------------------------------------------------------+
```

| Attribute | Description |
|---|---|
| Background | Full-width image (cover, centered) or gradient. Image set via theme settings. Semi-transparent dark overlay for text readability |
| Content | Centered vertically and horizontally, white text |
| Heading | H1 or H2 depending on context. Very large on desktop, progressively smaller on mobile. Bold, tight tracking |
| Subheading | Regular paragraph, medium size, generous top margin. Constrained to a comfortable reading width |
| CTA | Styled as primary button, large. Links to a configured collection or product |
| Height | Minimum 600px on desktop, 500px on tablet, 400px on mobile, 300px on small mobile |
| Padding | Generous vertical and horizontal padding |

**Mobile:** Same structure but stacked, with reduced font sizes and shorter minimum height. Full-bleed background image remains.

**Accessibility:**

- If background is an image, it is decorative (the content provides meaning)
- CTA button has descriptive text (e.g., "Shop the summer collection")

### 3.2 Featured Collections Section

**Layout:** Section with heading and grid of collection cards.

| Attribute | Description |
|---|---|
| Section heading | H2, large, bold, centered, with generous bottom margin |
| Grid | 2 columns on mobile, 4 columns on large screens, with gap spacing |
| Count | 2 to 4 collections, configurable in theme settings |

**Collection Card:**

```
+---------------------------+
|  [Collection Image]       |
|  3:4 aspect ratio         |
|  cover-fit, rounded       |
|                           |
|  [Title overlay at bottom]|
|  [Shop now ->]            |
+---------------------------+
```

| Element | Description |
|---|---|
| Image | 3:4 aspect ratio, cover-fit, rounded corners. Gradient overlay at bottom fading from transparent to semi-transparent dark |
| Title | White text overlaid at bottom of image, semi-bold, medium-large size |
| "Shop now" link | Small white text below title, slightly transparent, underlined. Becomes fully opaque on hover |
| Interaction | Entire card is clickable. Subtle scale-up on hover with smooth transition |

**Accessibility:**

- Each card link has the collection title as accessible name
- Images have alt text matching the collection title

### 3.3 Featured Products Section

**Livewire Component:** FeaturedProducts


Output:
Chunk ID: 1ddb3d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3435
Output:
# 05 - Business Logic Specification

> E-commerce platform - All domain logic, state machines, calculations, and rules for Laravel 12

---

## Conventions

| Convention | Detail |
|---|---|
| Monetary amounts | **INTEGER in minor units** (cents). All arithmetic uses integers - never floating point. |
| Percentage storage | Basis points where noted (e.g., 1900 = 19.00%) or whole-number percentage in `discounts.value_amount` (e.g., 15 = 15%) |
| Rounding | Round each line independently, then sum (prevents rounding drift) |
| Database | SQLite - single-writer, WAL mode, no advisory locks needed |
| Time | All timestamps in ISO-8601 UTC via Laravel `created_at` / `updated_at` |
| Currency | Single currency per store (`stores.default_currency`). No multi-currency conversion. |

---

## 1. Multi-Tenancy and Store Isolation

### 1.1 Tenant Resolution Middleware

**Class:** `App\Http\Middleware\ResolveStore`

**Register** in `bootstrap/app.php` under both the `storefront` and `admin` middleware groups.

#### Storefront Route Resolution (from request hostname)

1. Extract hostname from the incoming HTTP request.
2. Look up `store_domains` table where `hostname` matches.
3. Cache the hostname-to-store_id mapping using file cache with a 5-minute TTL. Cache key pattern: `store_domain:{hostname}`.
4. Retrieve the full Store model and bind it as a singleton in the application container under the key `current_store`.
5. If hostname is not found in `store_domains`: return HTTP 404.
6. If the resolved store has `status = 'suspended'`: return HTTP 503 maintenance page.

#### Admin Route Resolution (from session)

1. Read the selected store ID from the authenticated user's session (key: `current_store_id`).
2. Verify the authenticated user has a `store_users` record for that store.
3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.

```mermaid
flowchart TD
    A[Incoming Request] --> B{Route type?}
    B -->|Storefront| C[Extract hostname]
    C --> D{Hostname in cache?}
    D -->|Yes| E[Load store from cache]
    D -->|No| F[Query store_domains table]
    F --> G{Found?}
    G -->|No| H[Return 404]
    G -->|Yes| I[Cache hostname->store_id, 5 min TTL]
    I --> E
    E --> J{Store suspended?}
    J -->|Yes| K[Return 503 maintenance]
    J -->|No| L[Bind store as singleton, continue]

    B -->|Admin| M[Read store_id from session]
    M --> N{User has store_users record?}
    N -->|No| O[Return 403]
    N -->|Yes| P{Store suspended?}
    P -->|Yes| Q[Return 403 for mutations]
    P -->|No| L
```

### 1.2 Store Scoping

**Trait:** `App\Models\Concerns\BelongsToStore`
**Global Scope:** `App\Models\Scopes\StoreScope`

The trait applies a global query scope to every model that uses it. The scope adds a `WHERE store_id = <current_store_id>` clause to all queries automatically.

**Behavior:**

- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
- All SELECT, UPDATE, DELETE queries on tenant-scoped models include the store filter.
- In platform-admin contexts where cross-store access is needed, the scope can be temporarily removed.

**Models that must use this trait:**
products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

### 1.3 Role-Based Access Control

**Enum:** `App\Enums\StoreUserRole` - backed by string values: `owner`, `admin`, `staff`, `support`

**Permission Matrix:**

| Permission | Owner | Admin | Staff | Support |
|---|---|---|---|---|
| Manage store settings | Yes | Yes | No | No |
| Manage staff | Yes | Yes | No | No |
| Manage products | Yes | Yes | Yes | No |
| Manage orders | Yes | Yes | Yes | Yes (read-only) |
| Manage discounts | Yes | Yes | Yes | No |
| Manage themes | Yes | Yes | No | No |
| Manage apps | Yes | Yes | No | No |
| View analytics | Yes | Yes | Yes | No |
| Manage shipping/tax | Yes | Yes | No | No |
| Process refunds | Yes | Yes | No | No |
| Create fulfillments | Yes | Yes | Yes | No |

**Implementation requirements:**

- Create a Laravel policy for each resource model (e.g., `ProductPolicy`, `OrderPolicy`).
- Register gate definitions in a service provider.
- Each policy method receives the User, looks up the user's role for the current store via the `store_users` pivot table, and checks the matrix above.
- Add a helper method on the User model - `roleForStore(store)` - that returns the user's role for a given store or null if the user has no access.

---

## 2. Product Lifecycle

### 2.1 Status Transitions

**Enum:** `App\Enums\ProductStatus` - values: `draft`, `active`, `archived`

```mermaid
stateDiagram-v2
    [*] --> Draft : Product created
    Draft --> Active : Publish (requires variant with price > 0, title not empty)
    Draft --> Archived : Archive
    Active --> Archived : Archive (hide from storefront)
    Archived --> Active : Re-publish (same preconditions as Draft->Active)
    Active --> Draft : Revert (BLOCKED if order_lines exist)
    Archived --> Draft : Revert (BLOCKED if order_lines exist)
```

**Transition rules:**

| From | To | Preconditions |
|---|---|---|
| Draft | Active | At least one variant with `price_amount > 0`; product `title` is not empty |
| Draft | Archived | None |
| Active | Archived | None (soft transition: product hidden from storefront, preserved in order history) |
| Archived | Active | Same preconditions as Draft to Active |
| Active | Draft | **BLOCKED** if any `order_lines` reference variants of this product |
| Archived | Draft | **BLOCKED** if any `order_lines` reference variants of this product |

**Deletion rules:**
- Only `draft` products with zero `order_lines` references may be hard-deleted.
- Products with existing order references must be archived, never deleted, to preserve order history integrity.

**Service:** `App\Services\ProductService`
- Method `transitionStatus(product, newStatus)` validates the current state, checks preconditions, throws `InvalidProductTransitionException` on failure.
- On activation: set `published_at` to current time if not already set.
- Dispatch `ProductStatusChanged` event after successful transition.

### 2.2 Variant and Option Matrix

**Rules:**

- A product has 0 to 3 options (e.g., Size, Color, Material), stored in `product_options`.
- Each option has 1 or more values, stored in `product_option_values`.
- Variants represent the cartesian product of option values (or a manually curated subset).
- When a product has no options, a single "Default" variant is auto-created with `is_default = 1`.

**Matrix rebuild logic (when options change):**

```
PROCEDURE rebuildMatrix(product):
    current_option_values = load all option values grouped by option
    desired_combos = cartesianProduct(current_option_values)
    existing_variants = load all variants with their option value sets

    FOR EACH combo IN desired_combos:
        IF combo matches an existing variant's option value set:
            SKIP (preserve existing variant - prices, SKUs, inventory intact)
        ELSE:
            CREATE new variant with default pricing from first existing variant

    FOR EACH existing_variant NOT matching any desired combo:
        IF variant has order_line references:
            ARCHIVE variant (set status = 'archived')
        ELSE:
            DELETE variant
```

**Service:** `App\Services\VariantMatrixService`

### 2.3 SKU Uniqueness

- Combination of store_id (derived through the product's store) and SKU must be unique.
- Enforced at application level before save.
- Null or empty SKU values are exempt from the uniqueness check (multiple variants may have no SKU).
- Validation: query all variants belonging to products in the same store, check for SKU collision excluding the current variant's ID.

### 2.4 Handle/Slug Generation

**Helper class:** `App\Support\HandleGenerator`

- Auto-generate from the title by converting to a URL-safe slug (lowercase, hyphens for spaces, strip special characters).
- Uniqueness enforced per store via the database unique index on `(store_id, handle)`.
- On collision, append incrementing suffix: `my-product`, `my-product-1`, `my-product-2`, etc.
- Merchants can manually edit the handle; on save, re-validate uniqueness.
- Same logic applies to collections and pages.

---

## 3. Inventory Management

### 3.1 Tracking Model

Each `product_variant` has exactly one `inventory_item` record (created automatically when the variant is created).

| Field | Description |
|---|---|
| `quantity_on_hand` | Total physical stock count |
| `quantity_reserved` | Units held for active checkouts or pending orders |
| **Available** (computed) | `quantity_on_hand - quantity_reserved` |

### 3.2 Inventory Policy

Per-variant policy stored in `inventory_items.policy`:

| Policy | Behavior |
|---|---|
| `deny` | Block add-to-cart when available <= 0. Storefront displays "Out of stock". |
| `continue` | Allow add-to-cart regardless of stock level. When available <= 0, storefront displays "Available on backorder". |

### 3.3 Inventory Operations

**Service:** `App\Services\InventoryService`

All operations must execute within a database transaction.

```mermaid
flowchart LR
    subgraph "Stock Lifecycle"
        A[On Hand] -->|Reserve| B[Reserved]
        B -->|Commit on payment| C[Deducted]
        B -->|Release on expiry| A
        C -->|Restock on refund| A
    end
```

**Operation definitions:**

| Operation | Trigger | Effect on `quantity_on_hand` | Effect on `quantity_reserved` | Pre-check |
|---|---|---|---|---|
| **Reserve** | Checkout enters `payment_selected` | No change | +quantity | If policy=deny, verify available >= quantity. Throw `InsufficientInventoryException` if not. |
| **Release** | Checkout expires or is abandoned | No change | -quantity | None |
| **Commit** | Payment confirmed, order created | -quantity | -quantity | None (reservation already validated) |
| **Restock** | Refund processed with restock flag | +quantity | No change | None |

### 3.4 Concurrency

SQLite operates in single-writer mode with WAL journaling. Database transactions provide sufficient isolation. No optimistic locking or advisory locks are needed.

---

## 4. Cart Service

### 4.1 Cart Lifecycle

**Service:** `App\Services\CartService`

```mermaid
stateDiagram-v2
    [*] --> active : Cart created
    active --> converted : Checkout completed successfully
    active --> abandoned : Cleanup job (14 days inactive)
```

**Cart identification:**
- Guest users: cart ID stored in session cookie.
- Authenticated customers: cart linked via `customer_id` foreign key.

The guest cart is bound to the session using the key `cart_id`. When a guest visits the storefront, a cart is created on first add-to-cart and the cart's ID is stored in `session('cart_id')`. On customer login, if a session cart exists, it is merged with any existing customer cart (the session cart's items are added to the customer cart, preferring the higher quantity for duplicate variants). After merge, the session key is cleared.

**Cart merge on login:**

```
PROCEDURE mergeGuestCart(guestCart, customerCart):
    FOR EACH line IN guestCart.lines:
        existingLine = customerCart.findLineByVariant(line.variant_id)
        IF existingLine EXISTS:
            existingLine.quantity = MAX(existingLine.quantity, line.quantity)
            RECALCULATE existingLine amounts
        ELSE:
            MOVE line to customerCart
    MARK guestCart as abandoned
    RECALCULATE all line amounts on customerCart
```

### 4.2 Cart Operations

**Create cart:**
- Accepts: store, optional customer
- Sets: store_id, currency from store's default_currency, initial cart_version = 1

**Add line - validations (in order):**

1. Variant exists and belongs to the current store (through its product).
2. Product has status = `active`.
3. Variant has status = `active`.
4. Inventory policy check: if policy = `deny`, verify available >= requested quantity.
5. If the variant already exists in the cart, increment existing line quantity instead of creating a duplicate.
6. Set `unit_price_amount` from the variant's `price_amount`.
7. Calculate: `line_subtotal_amount = unit_price_amount * quantity`.
8. Set `line_discount_amount = 0` (recalculated by discount engine during checkout).
9. Calculate: `line_total_amount = line_subtotal_amount - line_discount_amount`.
10. Increment `cart_version`.

**Update quantity:**
- Validate new quantity > 0. If 0, remove the line instead.
- Re-check inventory policy for the new quantity.
- Recalculate line amounts.
- Increment `cart_version`.

**Remove line:**
- Delete the `cart_lines` record.
- Increment `cart_version`.

### 4.3 Cart Version (Optimistic Concurrency)

Every cart mutation increments `cart_version` by 1.

**For API clients:**
- Client sends `expected_version` with each mutation request.
- If cart's current version does not match the expected version, return HTTP 409 Conflict with the current cart state in the response body.
- This prevents race conditions when multiple browser tabs or devices modify the same cart.

**For Livewire/session-based flows:**
- Still implemented for consistency, but less critical since requests are serialized per session.

### 4.4 Line Amount Calculation

```
line_subtotal_amount = unit_price_amount * quantity
line_discount_amount = 0  (updated by discount engine during checkout)
line_total_amount    = line_subtotal_amount - line_discount_amount
```

All values are integers in minor currency units.

### 4.5 Cart Expiration

**Job:** `App\Jobs\CleanupAbandonedCarts`

- Schedule: daily


sed -n '1,260p' specs/08-PLAYWRIGHT-E2E-PLAN.md

# 08 - Playwright E2E Test Plan

> Complete end-to-end browser test specification for the e-commerce platform using Pest v4 browser testing (Playwright-backed). All tests verify the entire application works through a real browser. This document defines requirements and specifications for the coding agent to implement.

---

## Test Suite Summary

| Suite | File | Tests | Purpose | Dependencies |
|-------|------|:-----:|---------|--------------|
| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
| 2 - Admin Authentication | `tests/Browser/Admin/AuthenticationTest.php` | 10 | Admin login, logout, invalid credentials, session access control | Admin user |
| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
| 4 - Admin Order Management | `tests/Browser/Admin/OrderManagementTest.php` | 11 | Order listing, filtering, detail view, fulfillment, refund, confirm payment, fulfillment guard, mark shipped/delivered | Admin user, Orders (#1001, #1005), Customer |
| 5 - Admin Discount Management | `tests/Browser/Admin/DiscountManagementTest.php` | 6 | Discount code listing, creation, editing | Admin user, Discounts |
| 6 - Admin Settings | `tests/Browser/Admin/SettingsTest.php` | 7 | Store settings, shipping zones, tax config, domains | Admin user, Shipping zones, Store 1 |
| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
| 10 - Customer Account | `tests/Browser/Storefront/CustomerAccountTest.php` | 12 | Registration, login, order history, addresses, logout | Customer user, Orders (#1001/#1002/#1004) |
| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
| 12 - Tenant Isolation | `tests/Browser/Storefront/TenantIsolationTest.php` | 5 | Multi-store data isolation verification | Store 1, Admin user, Customer user |
| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
| 15 - Admin Collections | `tests/Browser/Admin/CollectionManagementTest.php` | 3 | Collection CRUD in admin | Admin user, Collections |
| 16 - Admin Customers | `tests/Browser/Admin/CustomerManagementTest.php` | 3 | Customer listing and detail views | Admin user, Customer user, Orders |
| 17 - Admin Pages | `tests/Browser/Admin/PageManagementTest.php` | 3 | CMS page management | Admin user, Pages |
| 18 - Admin Analytics | `tests/Browser/Admin/AnalyticsTest.php` | 3 | Analytics dashboard rendering | Admin user, Orders |
| **Total** | **18 files** | **143** | | |

---

## Test Infrastructure Requirements

### Framework

- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`

### Pest v4 Browser Testing API Reference

The coding agent should use these Pest v4 browser methods:

| Method | Purpose |
|--------|---------|
| `visit(url)` | Open a real browser and navigate to the given URL |
| `visit([url1, url2, ...])` | Visit multiple pages (for batch smoke testing) |
| `fill(field, value)` | Type a value into a named form field |
| `click(text)` | Click a visible element matching the given text |
| `assertSee(text)` | Assert the page contains the given visible text |
| `assertNoJavascriptErrors()` | Assert no uncaught JS exceptions occurred |
| `assertNoConsoleLogs()` | Assert no console warnings or errors were logged |

Additionally, standard Laravel test features are available within browser tests: factories, `Event::fake()`, `Notification::fake()`, `$this->actingAs()`, and model queries.

### Environment Configuration

The `.env.testing` file must contain the following keys:

| Key | Value | Purpose |
|-----|-------|---------|
| `APP_ENV` | `testing` | Laravel environment |
| `APP_URL` |…2059 chars truncated…|
| **Product #17** | Status: active, inventory: 0 on hand, policy: `deny` - displays "Sold out", add-to-cart disabled | 7, 11 |
| **Product #18** | Status: active, inventory: 0 on hand, policy: `continue` - displays "Available on backorder", add-to-cart enabled | 7, 11 |
| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
| **Collections** | "T-Shirts" (handle: `t-shirts`), "New Arrivals" (handle: `new-arrivals`), plus others | 1, 7, 13, 15 |
| **Orders** | #1001 (status: paid, fulfillment: unfulfilled, customer: customer@acme.test), #1002, #1004 also belonging to the same customer | 4, 10, 16, 18 |
| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |
| **Shipping zones** | "Domestic" zone (countries: DE) with rate "Standard Shipping" at 4.99 EUR flat; "International" zone with its own rates | 6, 9 |
| **Tax settings** | Store 1: manual mode, prices_include_tax varies by test | 6 |
| **Pages** | "About" (handle: `about`, status: published) | 1, 7, 17 |
| **Navigation** | Main menu with links to collections, about page | 7 |

---

## Test Execution Matrix

Tests should be executed in the order shown below. Later suites depend on the infrastructure and pages verified by earlier suites.

```mermaid
graph TD
    S1[Suite 1: Smoke Tests] --> S2[Suite 2: Admin Auth]
    S1 --> S7[Suite 7: Storefront Browsing]
    S2 --> S3[Suite 3: Admin Products]
    S2 --> S4[Suite 4: Admin Orders]
    S2 --> S5[Suite 5: Admin Discounts]
    S2 --> S6[Suite 6: Admin Settings]
    S2 --> S15[Suite 15: Admin Collections]
    S2 --> S16[Suite 16: Admin Customers]
    S2 --> S17[Suite 17: Admin Pages]
    S2 --> S18[Suite 18: Admin Analytics]
    S7 --> S8[Suite 8: Cart Flow]
    S8 --> S9[Suite 9: Checkout Flow]
    S7 --> S10[Suite 10: Customer Account]
    S7 --> S11[Suite 11: Inventory Enforcement]
    S7 --> S12[Suite 12: Tenant Isolation]
    S7 --> S13[Suite 13: Responsive / Mobile]
    S7 --> S14[Suite 14: Accessibility]
```

| Execution Phase | Suites | Rationale |
|-----------------|--------|-----------|
| Phase 1 | Suite 1 (Smoke) | Verifies all routes load before deeper testing |
| Phase 2 | Suites 2, 7 | Core auth and storefront browsing - gate to all further tests |
| Phase 3 | Suites 3, 4, 5, 6, 10, 11, 15, 16, 17, 18 | Admin CRUD and storefront feature suites (can run in parallel) |
| Phase 4 | Suite 8 (Cart) | Depends on storefront browsing working |
| Phase 5 | Suite 9 (Checkout) | Depends on cart flow working. Tests payment method selection (credit card, PayPal, bank transfer), magic card declines, and bank transfer instructions. |
| Phase 6 | Suites 12, 13, 14 | Cross-cutting concerns: isolation, responsive, accessibility |

---

## Test Directory Structure

```
tests/Browser/
    SmokeTest.php
    Admin/
        AuthenticationTest.php
        ProductManagementTest.php
        OrderManagementTest.php
        DiscountManagementTest.php
        SettingsTest.php
        CollectionManagementTest.php
        CustomerManagementTest.php
        PageManagementTest.php
        AnalyticsTest.php
    Storefront/
        BrowsingTest.php
        CartTest.php
        CheckoutTest.php
        CustomerAccountTest.php
        InventoryTest.php
        TenantIsolationTest.php
        ResponsiveTest.php
        AccessibilityTest.php
```

---

## Suite 1: Smoke Tests

**File:** `tests/Browser/SmokeTest.php`

**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.

---

### Test 1.1: Loads the storefront home page

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- No JavaScript errors

---

### Test 1.2: Loads a collection page

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- No JavaScript errors

---

### Test 1.3: Loads a product page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- No JavaScript errors

---

### Test 1.4: Loads the cart page

**Preconditions:** None

**Steps:**
1. Navigate to `/cart`

**Expected Results:**
- Page displays "Your Cart"
- No JavaScript errors

---

### Test 1.5: Loads the customer login page

**Preconditions:** None

**Steps:**
1. Navigate to `/account/login`

**Expected Results:**
- Page displays "Log in"
- No JavaScript errors

---

### Test 1.6: Loads the admin login page

**Preconditions:** None

**Steps:**
1. Navigate to `/admin/login`

**Expected Results:**
- Page displays "Sign in"
- No JavaScript errors

---

### Test 1.7: Loads the about page


Output:
Chunk ID: dc0ab1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3106
Output:
# 06 - Authentication and Security Specification

> E-commerce platform - Auth, authorization, and security controls for Laravel 12 + Livewire v4 + SQLite

---

## 1. Authentication

The platform supports three distinct authentication mechanisms: session-based admin auth, session-based customer auth (store-scoped), and token-based API auth via Sanctum.

### 1.1 Admin Authentication

| Setting | Value |
|---------|-------|
| Guard name | `web` (default Laravel guard) |
| Provider name | `users` (Eloquent driver) |
| Model | `App\Models\User` |
| Driver | Session (cookie-based) |
| Login route | `GET /admin/login` |
| Login form | Livewire component `Admin\Auth\Login` |

#### Admin Login Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant LW as Livewire Component
    participant Auth as Auth Guard (web)
    participant DB as Database
    participant S as Session

    B->>LW: Submit email + password
    LW->>LW: Validate input (email required, password required)
    LW->>LW: Check rate limiter (5/min per IP)
    alt Rate limit exceeded
        LW-->>B: Flash "Too many attempts. Try again in X seconds."
    end
    LW->>Auth: attempt(credentials, remember)
    Auth->>DB: SELECT from users WHERE email = ?
    Auth->>Auth: Verify bcrypt hash
    alt Credentials valid
        Auth->>S: Regenerate session ID (prevent fixation)
        Auth-->>LW: Success
        LW-->>B: Redirect to /admin (dashboard)
    else Credentials invalid
        Auth-->>LW: Failure
        LW-->>B: Flash "Invalid credentials" (generic, never reveal which field)
    end
```

**Requirements:**

- The login form collects `email` and `password`.
- On successful login, the session MUST be regenerated to prevent session fixation attacks.
- On failed login, only the generic message "Invalid credentials" is shown. The system must not indicate whether the email or password was wrong.
- Rate limiting: maximum 5 attempts per minute, keyed by IP address. Uses the `login` rate limiter.
- After login: redirect to `/admin` (the dashboard).

#### Remember Me

- An optional "Remember me" checkbox on the login form.
- When enabled, Laravel sets a long-lived remember token cookie.
- The `remember_token` column on the `users` table stores the hashed token.
- Cookie name follows Laravel default convention: `remember_web_{hash}`.

#### Admin Password Reset Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant App as Application
    participant DB as Database
    participant Mail as Mail Service

    B->>App: GET /admin/forgot-password
    App-->>B: Render forgot-password form
    B->>App: POST /admin/forgot-password (email)
    App->>DB: Look up user by email
    alt User exists
        App->>DB: Store reset token in password_reset_tokens table
        App->>Mail: Send reset link email
    end
    App-->>B: "If that email exists, we sent a reset link." (generic)
    B->>App: GET /admin/reset-password/{token}
    App-->>B: Render reset-password form
    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
    App->>DB: Validate token (exists, not expired, matches email)
    alt Valid token
        App->>DB: Update user password (bcrypt hash)
        App->>DB: Delete used token
        App-->>B: Redirect to /admin/login with success flash
    else Invalid/expired token
        App-->>B: Error message
    end
```

**Requirements:**

- Uses Laravel's built-in password broker (`users` broker).
- Reset tokens stored in the `password_reset_tokens` table.
- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
- Throttle: one reset email per 60 seconds per email address.
- The "forgot password" response must be generic (do not reveal whether email exists).

#### Admin Logout

- Route: `POST /admin/logout` (via Livewire action or standard route).
- Steps: invalidate the entire session, regenerate the CSRF token, redirect to `/admin/login`.
- The response must not cache.

#### Session Lifetime

| Setting | Value |
|---------|-------|
| Lifetime | 120 minutes (configurable via `SESSION_LIFETIME` env var) |
| Expire on close | No (session persists until lifetime expires) |

---

### 1.2 Customer Authentication

| Setting | Value |
|---------|-------|
| Guard name | `customer` (custom guard) |
| Provider name | `customers` (Eloquent driver) |
| Model | `App\Models\Customer` |
| Driver | Session (cookie-based) |
| Login route | `GET /account/login` |
| Registration route | `GET /account/register` |

#### Customer Login Flow

```mermaid
sequenceDiagram
    participant B as Browser
    participant LW as Livewire Component
    participant Auth as Auth Guard (customer)
    participant Store as ResolveStore Middleware
    participant DB as Database

    Note over Store: Store already resolved from hostname
    B->>LW: Submit email + password
    LW->>LW: Validate input
    LW->>LW: Check rate limiter (5/min per IP)
    LW->>Auth: attempt(credentials + store_id, remember)
    Auth->>DB: SELECT from customers WHERE email = ? AND store_id = ?
    Auth->>Auth: Verify bcrypt hash
    alt Valid
        Auth-->>LW: Success (regenerate session)
        LW-->>B: Redirect to /account (or intended URL)
    else Invalid
        Auth-->>LW: Failure
        LW-->>B: Flash "Invalid credentials"
    end
```

**Requirements:**

- Customer login is always scoped to the current store. The store is resolved from the request hostname by the `ResolveStore` middleware before the login attempt.
- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
- The same email address can exist in different stores (multi-tenant isolation).
- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).

#### Customer Registration Flow

```mermaid
flowchart TD
    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
    B --> C{Validate via RegisterCustomerRequest}
    C -- Invalid --> D[Show validation errors]
    C -- Valid --> E[Create Customer record scoped to current store]
    E --> F[Auto-login via customer guard]
    F --> G[Redirect to /account]
```

**Registration fields:**

| Field | Type | Required | Validation |
|-------|------|:--------:|------------|
| name | string | Y | max:255 |
| email | string | Y | valid email, unique per store (composite unique on email + store_id) |
| password | string | Y | min:8, confirmed |
| password_confirmation | string | Y | must match password |
| marketing_opt_in | boolean | N | defaults to false |

#### Customer Store Scoping

- The `customers` auth provider requires a custom `CustomerUserProvider` class.
- This provider overrides the default Eloquent provider to always include `store_id` in credential queries.
- The `store_id` is obtained from the container-bound current store (resolved by `ResolveStore` middleware).
- This ensures that `Auth::guard('customer')->attempt()` never crosses store boundaries.

#### Customer Password Reset

- Uses a separate password broker: `customers` broker.
- Tokens stored in the `customer_password_reset_tokens` table (separate from admin tokens).
- Token records include `store_id` to scope resets to the correct store.
- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
- Token expiration: 60 minutes.
- Throttle: one reset email per 60 seconds.
- Generic response on the "forgot password" form (do not reveal whether email exists in the store).

---

### 1.3 API Authentication (Sanctum)

| Setting | Value |
|---------|-------|
| Guard name | `sanctum` |
| Provider | `users` (admin users only) |
| Token type | Personal access tokens |
| Token prefix | `shop_` |
| Hashing | SHA-256 |
| Default expiration | 1 year |
| Storage | `personal_access_tokens` table |

#### Token Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Created: Admin creates token in Developers section
    Created --> Active: Plain-text shown once, hash stored
    Active --> InUse: Token sent as Bearer header
    InUse --> Active: Request processed
    Active --> Revoked: Admin revokes token
    Active --> Expired: Expiration date reached
    Revoked --> [*]: Token row deleted
    Expired --> [*]: Token row deleted (cleanup job)
```

**Requirements:**

- Tokens are created in the admin panel, under the Developers section.
- Each token has: a human-readable name, an optional expiration date, and a list of abilities (scopes).
- On creation, the plain-text token (prefixed with `shop_`) is displayed exactly once. Only the SHA-256 hash is persisted.
- Tokens are sent as `Authorization: Bearer shop_xxxxxxxx`.
- Sanctum middleware validates the hash and resolves the associated user.
- Ability checks are performed per-endpoint (e.g., the products list endpoint requires `read-products`).
- Revoking a token deletes its row from the `personal_access_tokens` table.

#### Token Abilities (Scopes)

| Ability | Description |
|---------|-------------|
| `read-products` | List and view products |
| `write-products` | Create, update, delete products |
| `read-orders` | List and view orders |
| `write-orders` | Update orders, create fulfillments |
| `read-customers` | List and view customers |
| `write-customers` | Update customers |
| `read-collections` | List and view collections |
| `write-collections` | Create, update, delete collections |
| `read-discounts` | List and view discounts |
| `write-discounts` | Create, update, delete discounts |
| `read-analytics` | View analytics data |
| `read-settings` | View store settings (general, shipping, taxes, checkout, notifications) |
| `write-settings` | Update store settings (replaces the legacy `manage-store`, `manage-shipping`, `manage-taxes` scopes) |
| `read-themes` | View themes and theme files |
| `write-themes` | Create, update, delete, publish themes |
| `read-content` | View pages and navigation menus |
| `write-content` | Create, update, delete pages and navigation items |
| `manage-platform` | Platform-level management (super-admin only) |

Each API endpoint must check the token's abilities before processing the request. If the token lacks the required ability, the endpoint returns `403 Forbidden`.

---

### 1.4 Guard Configuration

File to configure: `config/auth.php`

#### Guards Data Dictionary

| Guard Name | Driver | Provider | Purpose |
|------------|--------|----------|---------|
| `web` | session | `users` | Admin panel session auth (default guard) |
| `customer` | session | `customers` | Storefront customer session auth |
| `sanctum` | sanctum | `users` | Admin REST API token auth |

#### Providers Data Dictionary

| Provider Name | Driver | Model | Purpose |
|---------------|--------|-------|---------|
| `users` | eloquent | `App\Models\User` | Admin users |
| `customers` | eloquent | `App\Models\Customer` | Storefront customers (store-scoped via custom provider) |

#### Password Brokers Data Dictionary

| Broker Name | Provider | Token Table | Expiry | Throttle |
|-------------|----------|-------------|--------|----------|
| `users` | `users` | `password_reset_tokens` | 60 min | 60 sec |
| `customers` | `customers` | `customer_password_reset_tokens` | 60 min | 60 sec |

**Defaults:** The default guard is `web` and the default password broker is `users`.

---

## 2. Authorization

### 2.1 Role Enum

File: `app/Enums/StoreUserRole.php`

A backed string enum with four values:

| Case | Stored Value | Description |
|------|-------------|-------------|
| Owner | `'owner'` | Full access. Can transfer ownership, manage billing, delete store. One per store. |
| Admin | `'admin'` | Near-full access. Cannot transfer ownership or delete store. |
| Staff | `'staff'` | Operational access. Can manage products, orders, collections, discounts, pages. Cannot access settings, themes, staff management, or developers. |
| Support | `'support'` | Read-only access to products, orders, and customers. For customer service agents. |

**Constraints:**

- Each store must have exactly one Owner.
- Ownership transfer is a dedicated action (not a simple role change).
- A User can hold different roles in different stores (via the `store_users` pivot table).

---


Output:
Chunk ID: 60b8e4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1845
Output:
# 07 - Seeders and Test Data Specification

> Complete seeder and factory specification for populating the platform with realistic demo data. Laravel 12, SQLite. All monetary amounts in cents (INTEGER). All seed data supports Playwright E2E tests.

---

## 1. DatabaseSeeder Orchestration

The main `DatabaseSeeder` calls child seeders in a strict order that respects foreign key dependencies. Each seeder targets a specific entity type. The ordering ensures parent records exist before children are created.

**Execution order:**

| Step | Seeder Class | Depends On | Purpose |
|------|-------------|------------|---------|
| 1 | OrganizationSeeder | (none) | Creates billing organizations |
| 2 | StoreSeeder | Organizations | Creates stores under organizations |
| 3 | StoreDomainSeeder | Stores | Attaches hostnames to stores |
| 4 | UserSeeder | (none) | Creates admin/staff user accounts |
| 5 | StoreUserSeeder | Stores, Users | Links users to stores with roles (pivot) |
| 6 | StoreSettingsSeeder | Stores | Inserts per-store JSON settings |
| 7 | TaxSettingsSeeder | Stores | Configures tax mode and rates |
| 8 | ShippingSeeder | Stores | Creates shipping zones and rates |
| 9 | CollectionSeeder | Stores | Creates product collections |
| 10 | ProductSeeder | Stores, Collections | Creates products, options, variants, inventory, collection assignments |
| 11 | DiscountSeeder | Stores | Creates discount codes |
| 12 | CustomerSeeder | Stores | Creates customer accounts with addresses |
| 13 | OrderSeeder | Stores, Customers, Products, Variants, Discounts | Creates orders with lines, payments, fulfillments, refunds |
| 14 | ThemeSeeder | Stores | Creates themes with settings |
| 15 | PageSeeder | Stores | Creates content pages |
| 16 | NavigationSeeder | Stores, Collections, Pages | Creates menus linking to collections and pages |
| 17 | AnalyticsSeeder | Stores, Customers | Creates daily aggregates and event stream |
| 18 | SearchSettingsSeeder | Stores | Configures search synonyms and stop words |

**Dependency diagram (Mermaid):**

```mermaid
graph TD
    A[OrganizationSeeder] --> B[StoreSeeder]
    B --> C[StoreDomainSeeder]
    B --> F[StoreSettingsSeeder]
    B --> G[TaxSettingsSeeder]
    B --> H[ShippingSeeder]
    B --> I[CollectionSeeder]
    D[UserSeeder] --> E[StoreUserSeeder]
    B --> E
    B --> J[ProductSeeder]
    I --> J
    B --> K[DiscountSeeder]
    B --> L[CustomerSeeder]
    J --> M[OrderSeeder]
    L --> M
    K --> M
    B --> N[ThemeSeeder]
    B --> O[PageSeeder]
    I --> P[NavigationSeeder]
    O --> P
    B --> Q[AnalyticsSeeder]
    L --> Q
    B --> R[SearchSettingsSeeder]
```

**Running seeders:**
- All seeders: `php artisan db:seed`
- Single seeder: `php artisan db:seed --class=ProductSeeder`
- Fresh database with seed data: `php artisan migrate:fresh --seed`

---

## 2. Factory Definitions

Each factory below lists the model, default field generation rules, named states (overrides), and relationship hooks. The coding agent should create corresponding Laravel factory classes.

---

### 2.1 OrganizationFactory

**Model:** Organization

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| name | Faker company name |
| billing_email | Faker company email |

**States:** None.

---

### 2.2 StoreFactory

**Model:** Store

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| organization_id | Creates a new Organization via its factory |
| name | Faker company name + " Store" suffix |
| handle | Faker unique slug (2 words) |
| status | "active" |
| default_currency | "EUR" |
| default_locale | "en" |
| timezone | "Europe/Berlin" |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `suspended` | status = "suspended" |

---

### 2.3 StoreDomainFactory

**Model:** StoreDomain

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| hostname | Faker unique domain name |
| type | "storefront" |
| is_primary | 1 |
| tls_mode | "managed" |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `admin` | type = "admin" |
| `api` | type = "api" |
| `secondary` | is_primary = 0 |

---

### 2.4 UserFactory

**Model:** User

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| email | Faker unique safe email |
| password_hash | bcrypt of "password" |
| name | Faker full name |
| status | "active" |
| last_login_at | Random date within the last 30 days |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `disabled` | status = "disabled" |

---

### 2.5 ProductFactory

**Model:** Product

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| title | Faker 3 random words joined |
| handle | URL-slug of the title |
| status | "active" |
| description_html | Faker 2 paragraphs wrapped in `<p>` tags |
| vendor | Faker company name |
| product_type | Random from: Shirts, Pants, Shoes, Accessories, Electronics, Books |
| tags | JSON array of 1-3 random picks from: new, sale, trending, popular, limited |
| published_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `draft` | status = "draft", published_at = null |
| `archived` | status = "archived" |

**Relationship hooks:**

| Hook Name | Behavior |
|-----------|----------|
| `withVariants(count)` | After creating the product, create `count` ProductVariant records associated with the product |
| `withDefaultVariant(price)` | After creating the product, create one ProductVariant marked as default at the given price in EUR |

---

### 2.6 ProductVariantFactory

**Model:** ProductVariant

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| product_id | Creates a new Product via its factory |
| sku | Faker unique, pattern: "SKU-####-???" (digits and letters) |
| barcode | Faker EAN-13 |
| price_amount | Random integer 999-19999 (9.99 to 199.99 EUR) |
| compare_at_amount | null |
| currency | "EUR" |
| weight_g | Random integer 100-5000 |
| requires_shipping | 1 |
| is_default | 0 |
| position | 0 |
| status | "active" |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `onSale` | compare_at_amount = random 20000-39999, price_amount = random 9999-19999 |
| `digital` | requires_shipping = 0, weight_g = 0 |
| `default` | is_default = 1 |
| `archived` | status = "archived" |

---

### 2.7 InventoryItemFactory

**Model:** InventoryItem

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| quantity_on_hand | Random integer 0-100 |
| quantity_reserved | 0 |
| policy | "deny" |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `outOfStock` | quantity_on_hand = 0 |
| `continuePolicy` | policy = "continue" |
| `lowStock` | quantity_on_hand = random 1-3 |

---

### 2.8 CollectionFactory

**Model:** Collection

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| title | Faker 2 random words joined |
| handle | URL-slug of the title |
| description_html | Faker sentence wrapped in `<p>` tags |


Output:
Chunk ID: 7e0859
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3015
Output:
# 08 - Playwright E2E Test Plan

> Complete end-to-end browser test specification for the e-commerce platform using Pest v4 browser testing (Playwright-backed). All tests verify the entire application works through a real browser. This document defines requirements and specifications for the coding agent to implement.

---

## Test Suite Summary

| Suite | File | Tests | Purpose | Dependencies |
|-------|------|:-----:|---------|--------------|
| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
| 2 - Admin Authentication | `tests/Browser/Admin/AuthenticationTest.php` | 10 | Admin login, logout, invalid credentials, session access control | Admin user |
| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
| 4 - Admin Order Management | `tests/Browser/Admin/OrderManagementTest.php` | 11 | Order listing, filtering, detail view, fulfillment, refund, confirm payment, fulfillment guard, mark shipped/delivered | Admin user, Orders (#1001, #1005), Customer |
| 5 - Admin Discount Management | `tests/Browser/Admin/DiscountManagementTest.php` | 6 | Discount code listing, creation, editing | Admin user, Discounts |
| 6 - Admin Settings | `tests/Browser/Admin/SettingsTest.php` | 7 | Store settings, shipping zones, tax config, domains | Admin user, Shipping zones, Store 1 |
| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
| 10 - Customer Account | `tests/Browser/Storefront/CustomerAccountTest.php` | 12 | Registration, login, order history, addresses, logout | Customer user, Orders (#1001/#1002/#1004) |
| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
| 12 - Tenant Isolation | `tests/Browser/Storefront/TenantIsolationTest.php` | 5 | Multi-store data isolation verification | Store 1, Admin user, Customer user |
| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
| 15 - Admin Collections | `tests/Browser/Admin/CollectionManagementTest.php` | 3 | Collection CRUD in admin | Admin user, Collections |
| 16 - Admin Customers | `tests/Browser/Admin/CustomerManagementTest.php` | 3 | Customer listing and detail views | Admin user, Customer user, Orders |
| 17 - Admin Pages | `tests/Browser/Admin/PageManagementTest.php` | 3 | CMS page management | Admin user, Pages |
| 18 - Admin Analytics | `tests/Browser/Admin/AnalyticsTest.php` | 3 | Analytics dashboard rendering | Admin user, Orders |
| **Total** | **18 files** | **143** | | |

---

## Test Infrastructure Requirements

### Framework

- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`

### Pest v4 Browser Testing API Reference

The coding agent should use these Pest v4 browser methods:

| Method | Purpose |
|--------|---------|
| `visit(url)` | Open a real browser and navigate to the given URL |
| `visit([url1, url2, ...])` | Visit multiple pages (for batch smoke testing) |
| `fill(field, value)` | Type a value into a named form field |
| `click(text)` | Click a visible element matching the given text |
| `assertSee(text)` | Assert the page contains the given visible text |
| `assertNoJavascriptErrors()` | Assert no uncaught JS exceptions occurred |
| `assertNoConsoleLogs()` | Assert no console warnings or errors were logged |

Additionally, standard Laravel test features are available within browser tests: factories, `Event::fake()`, `Notification::fake()`, `$this->actingAs()`, and model queries.

### Environment Configuration

The `.env.testing` file must contain the following keys:

| Key | Value | Purpose |
|-----|-------|---------|
| `APP_ENV` | `testing` | Laravel environment |
| `APP_URL` | `http://acme-fashion.test` | Browser base URL (served by Herd) |
| `DB_CONNECTION` | `sqlite` | Database driver |
| `DB_DATABASE` | Absolute path to test SQLite file | Isolated test database |
| `PAYMENT_PROVIDER` | `mock` | Mock PSP - no external API keys needed |
| `MAIL_MAILER` | `array` | Trap emails in memory |
| `QUEUE_CONNECTION` | `sync` | Process jobs synchronously |

### Prerequisites Before Running

1. Database seeded with `php artisan migrate:fresh --seed`
2. Laravel Herd serving `acme-fashion.test`
3. Playwright browsers installed via `npx playwright install`
4. Frontend assets built via `npm run build`

---

## Seeded Data Dependency Table

All tests depend on the database seeder providing this baseline data. Each test gets a fresh database via `RefreshDatabase`, then the seeder repopulates it.

Each browser test file must set `protected bool $seed = true;` on the test class (or call `$this->seed(DatabaseSeeder::class)` in `beforeEach`) to ensure the database is populated after RefreshDatabase clears it. Laravel's `RefreshDatabase` trait does NOT automatically run seeders unless explicitly configured.

| Entity | Key Details | Used By Suites |
|--------|-------------|----------------|
| **Admin user** | Email: `admin@acme.test`, password: `password`, role: `owner` on Store 1 | 2, 3, 4, 5, 6, 12, 13, 15, 16, 17, 18 |
| **Customer user** | Email: `customer@acme.test`, password: `password`, name: "John Doe", belongs to Store 1 | 10, 12 |
| **Store 1** | Name: "Acme Fashion", handle: `acme-fashion`, domain: `acme-fashion.test`, currency: EUR | 1, 6, 7, 12, 14 |
| **Product #1** | Title: "Classic Cotton T-Shirt", handle: `classic-cotton-t-shirt`, price: 24.99 EUR, status: active, options: Size (S/M/L/XL), Color (Black/White/Navy), inventory: in stock | 1, 3, 7, 8, 9, 11, 13, 14 |
| **Product #2** | Title: "Premium Slim Fit Jeans", handle: `premium-slim-fit-jeans`, status: active, has compare-at pricing (sale), options: Size, Color | 7, 8 |
| **Product #15** | Status: `draft` - must NOT appear on storefront | 3, 7 |
| **Product #17** | Status: active, inventory: 0 on hand, policy: `deny` - displays "Sold out", add-to-cart disabled | 7, 11 |
| **Product #18** | Status: active, inventory: 0 on hand, policy: `continue` - displays "Available on backorder", add-to-cart enabled | 7, 11 |
| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
| **Collections** | "T-Shirts" (handle: `t-shirts`), "New Arrivals" (handle: `new-arrivals`), plus others | 1, 7, 13, 15 |
| **Orders** | #1001 (status: paid, fulfillment: unfulfilled, customer: customer@acme.test), #1002, #1004 also belonging to the same customer | 4, 10, 16, 18 |
| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |
| **Shipping zones** | "Domestic" zone (countries: DE) with rate "Standard Shipping" at 4.99 EUR flat; "International" zone with its own rates | 6, 9 |
| **Tax settings** | Store 1: manual mode, prices_include_tax varies by test | 6 |
| **Pages** | "About" (handle: `about`, status: published) | 1, 7, 17 |
| **Navigation** | Main menu with links to collections, about page | 7 |

---

## Test Execution Matrix

Tests should be executed in the order shown below. Later suites depend on the infrastructure and pages verified by earlier suites.

```mermaid
graph TD
    S1[Suite 1: Smoke Tests] --> S2[Suite 2: Admin Auth]
    S1 --> S7[Suite 7: Storefront Browsing]
    S2 --> S3[Suite 3: Admin Products]
    S2 --> S4[Suite 4: Admin Orders]
    S2 --> S5[Suite 5: Admin Discounts]
    S2 --> S6[Suite 6: Admin Settings]
    S2 --> S15[Suite 15: Admin Collections]
    S2 --> S16[Suite 16: Admin Customers]
    S2 --> S17[Suite 17: Admin Pages]
    S2 --> S18[Suite 18: Admin Analytics]
    S7 --> S8[Suite 8: Cart Flow]
    S8 --> S9[Suite 9: Checkout Flow]
    S7 --> S10[Suite 10: Customer Account]
    S7 --> S11[Suite 11: Inventory Enforcement]
    S7 --> S12[Suite 12: Tenant Isolation]
    S7 --> S13[Suite 13: Responsive / Mobile]
    S7 --> S14[Suite 14: Accessibility]
```

| Execution Phase | Suites | Rationale |
|-----------------|--------|-----------|
| Phase 1 | Suite 1 (Smoke) | Verifies all routes load before deeper testing |
| Phase 2 | Suites 2, 7 | Core auth and storefront browsing - gate to all further tests |
| Phase 3 | Suites 3, 4, 5, 6, 10, 11, 15, 16, 17, 18 | Admin CRUD and storefront feature suites (can run in parallel) |
| Phase 4 | Suite 8 (Cart) | Depends on storefront browsing working |
| Phase 5 | Suite 9 (Checkout) | Depends on cart flow working. Tests payment method selection (credit card, PayPal, bank transfer), magic card declines, and bank transfer instructions. |
| Phase 6 | Suites 12, 13, 14 | Cross-cutting concerns: isolation, responsive, accessibility |

---

## Test Directory Structure

```
tests/Browser/
    SmokeTest.php
    Admin/
        AuthenticationTest.php
        ProductManagementTest.php
        OrderManagementTest.php
        DiscountManagementTest.php
        SettingsTest.php
        CollectionManagementTest.php
        CustomerManagementTest.php
        PageManagementTest.php
        AnalyticsTest.php
    Storefront/
        BrowsingTest.php
        CartTest.php
        CheckoutTest.php
        CustomerAccountTest.php
        InventoryTest.php
        TenantIsolationTest.php
        ResponsiveTest.php
        AccessibilityTest.php
```

---

## Suite 1: Smoke Tests

**File:** `tests/Browser/SmokeTest.php`

**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.

---

### Test 1.1: Loads the storefront home page

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- No JavaScript errors

---

### Test 1.2: Loads a collection page

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- No JavaScript errors

---

### Test 1.3: Loads a product page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- No JavaScript errors

---

### Test 1.4: Loads the cart page

**Preconditions:** None

**Steps:**
1. Navigate to `/cart`

**Expected Results:**
- Page displays "Your Cart"
- No JavaScript errors

---

### Test 1.5: Loads the customer login page

**Preconditions:** None

**Steps:**
1. Navigate to `/account/login`

**Expected Results:**
- Page displays "Log in"
- No JavaScript errors

---

### Test 1.6: Loads the admin login page

**Preconditions:** None

**Steps:**
1. Navigate to `/admin/login`

**Expected Results:**
- Page displays "Sign in"
- No JavaScript errors

---

### Test 1.7: Loads the about page


laravel/framework livewire/livewire pestphp/pest livewire/flux

middleware groups bootstrap app Laravel 12

custom auth guard user provider password broker

database migrations foreign keys check constraints sqlite

Livewire full page components routes form validation