   300→**Model:** CartLine
   301→
   302→**Default definition:**
   303→
   304→| Field | Generation Rule |
   305→|-------|----------------|
   306→| cart_id | Creates a new Cart via its factory |
   307→| variant_id | Creates a new ProductVariant via its factory |
   308→| quantity | Random 1-5 |
   309→| unit_price_amount | Random 999-19999 |
   310→| line_subtotal_amount | Computed: unit_price_amount * quantity |
   311→| line_discount_amount | 0 |
   312→| line_total_amount | Computed: line_subtotal_amount - line_discount_amount |
   313→
   314→**States:** None.
   315→
   316→---
   317→
   318→### 2.11 CheckoutFactory
   319→
   320→**Model:** Checkout
   321→
   322→**Default definition:**
   323→
   324→| Field | Generation Rule |
   325→|-------|----------------|
   326→| store_id | Creates a new Store via its factory |
   327→| cart_id | Creates a new Cart via its factory |
   328→| customer_id | null |
   329→| status | "started" |
   330→| email | Faker safe email |
   331→| shipping_address_json | JSON object with Faker first_name, last_name, address1, city, country_code "DE", zip |
   332→| billing_address_json | null |
   333→| shipping_method_id | null |
   334→| discount_code | null |
   335→| tax_provider_snapshot_json | null |
   336→| totals_json | null |
   337→| expires_at | 24 hours from now |
   338→
   339→**States:**
   340→
   341→| State Name | Overrides |
   342→|-----------|-----------|
   343→| `completed` | status = "completed" |
   344→| `expired` | status = "expired", expires_at = 1 hour ago |
   345→
   346→---
   347→
   348→### 2.12 OrderFactory
   349→
   350→**Model:** Order
   351→
   352→**Default definition:**
   353→
   354→| Field | Generation Rule |
   355→|-------|----------------|
   356→| store_id | Creates a new Store via its factory |
   357→| customer_id | Creates a new Customer via its factory |
   358→| order_number | "#" + Faker unique integer 1001-9999 |
   359→| status | "paid" |
   360→| financial_status | "paid" |
   361→| fulfillment_status | "unfulfilled" |
   362→| currency | "EUR" |
   363→| subtotal_amount | 4998 |
   364→| discount_amount | 0 |
   365→| shipping_amount | 499 |
   366→| tax_amount | 798 |
   367→| total_amount | 5497 |
   368→| email | Faker safe email |
   369→| billing_address_json | JSON address object with German Faker data |
   370→| shipping_address_json | JSON address object with German Faker data |
   371→| placed_at | Current timestamp |
   372→
   373→**States:**
   374→
   375→| State Name | Overrides |
   376→|-----------|-----------|
   377→| `pending` | status = "pending", financial_status = "pending" |
   378→| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
   379→| `cancelled` | status = "cancelled", financial_status = "refunded" |
   380→| `refunded` | status = "refunded", financial_status = "refunded" |
   381→| `partiallyFulfilled` | fulfillment_status = "partial" |
   382→
   383→---
   384→
   385→### 2.13 OrderLineFactory
   386→
   387→**Model:** OrderLine
   388→
   389→**Default definition:**
   390→
   391→| Field | Generation Rule |
   392→|-------|----------------|
   393→| order_id | Creates a new Order via its factory |
   394→| product_id | Creates a new Product via its factory |
   395→| variant_id | Creates a new ProductVariant via its factory |
   396→| title_snapshot | Faker 3 words |
   397→| sku_snapshot | Faker pattern "SKU-####-???" |
   398→| quantity | Random 1-3 |
   399→| unit_price_amount | Random 999-19999 |

**Model:** CartLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| cart_id | Creates a new Cart via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| quantity | Random 1-5 |
| unit_price_amount | Random 999-19999 |
| line_subtotal_amount | Computed: unit_price_amount * quantity |
| line_discount_amount | 0 |
| line_total_amount | Computed: line_subtotal_amount - line_discount_amount |

**States:** None.

---

### 2.11 CheckoutFactory

**Model:** Checkout

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| cart_id | Creates a new Cart via its factory |
| customer_id | null |
| status | "started" |
| email | Faker safe email |
| shipping_address_json | JSON object with Faker first_name, last_name, address1, city, country_code "DE", zip |
| billing_address_json | null |
| shipping_method_id | null |
| discount_code | null |
| tax_provider_snapshot_json | null |
| totals_json | null |
| expires_at | 24 hours from now |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `completed` | status = "completed" |
| `expired` | status = "expired", expires_at = 1 hour ago |

---

### 2.12 OrderFactory

**Model:** Order

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| store_id | Creates a new Store via its factory |
| customer_id | Creates a new Customer via its factory |
| order_number | "#" + Faker unique integer 1001-9999 |
| status | "paid" |
| financial_status | "paid" |
| fulfillment_status | "unfulfilled" |
| currency | "EUR" |
| subtotal_amount | 4998 |
| discount_amount | 0 |
| shipping_amount | 499 |
| tax_amount | 798 |
| total_amount | 5497 |
| email | Faker safe email |
| billing_address_json | JSON address object with German Faker data |
| shipping_address_json | JSON address object with German Faker data |
| placed_at | Current timestamp |

**States:**

| State Name | Overrides |
|-----------|-----------|
| `pending` | status = "pending", financial_status = "pending" |
| `fulfilled` | status = "fulfilled", fulfillment_status = "fulfilled" |
| `cancelled` | status = "cancelled", financial_status = "refunded" |
| `refunded` | status = "refunded", financial_status = "refunded" |
| `partiallyFulfilled` | fulfillment_status = "partial" |

---

### 2.13 OrderLineFactory

**Model:** OrderLine

**Default definition:**

| Field | Generation Rule |
|-------|----------------|
| order_id | Creates a new Order via its factory |
| product_id | Creates a new Product via its factory |
| variant_id | Creates a new ProductVariant via its factory |
| title_snapshot | Faker 3 words |
| sku_snapshot | Faker pattern "SKU-####-???" |
| quantity | Random 1-3 |
| unit_price_amount | Random 999-19999 |


   200→
   201→**Expected Results:**
   202→- Page displays "T-Shirts"
   203→- No JavaScript errors
   204→
   205→---
   206→
   207→### Test 1.3: Loads a product page
   208→
   209→**Preconditions:** Seeded products
   210→
   211→**Steps:**
   212→1. Navigate to `/products/classic-cotton-t-shirt`
   213→
   214→**Expected Results:**
   215→- Page displays "Classic Cotton T-Shirt"
   216→- Page displays "24.99"
   217→- No JavaScript errors
   218→
   219→---
   220→
   221→### Test 1.4: Loads the cart page
   222→
   223→**Preconditions:** None
   224→
   225→**Steps:**
   226→1. Navigate to `/cart`
   227→
   228→**Expected Results:**
   229→- Page displays "Your Cart"
   230→- No JavaScript errors
   231→
   232→---
   233→
   234→### Test 1.5: Loads the customer login page
   235→
   236→**Preconditions:** None
   237→
   238→**Steps:**
   239→1. Navigate to `/account/login`
   240→
   241→**Expected Results:**
   242→- Page displays "Log in"
   243→- No JavaScript errors
   244→
   245→---
   246→
   247→### Test 1.6: Loads the admin login page
   248→
   249→**Preconditions:** None
   250→
   251→**Steps:**
   252→1. Navigate to `/admin/login`
   253→
   254→**Expected Results:**
   255→- Page displays "Sign in"
   256→- No JavaScript errors
   257→
   258→---
   259→
   260→### Test 1.7: Loads the about page
   261→
   262→**Preconditions:** Seeded pages
   263→
   264→**Steps:**
   265→1. Navigate to `/pages/about`
   266→
   267→**Expected Results:**
   268→- Page displays "About"
   269→- No JavaScript errors
   270→
   271→---
   272→
   273→### Test 1.8: Loads the search page
   274→
   275→**Preconditions:** Seeded products
   276→
   277→**Steps:**
   278→1. Navigate to `/search?q=shirt`
   279→
   280→**Expected Results:**
   281→- Page displays "shirt"
   282→- No JavaScript errors
   283→
   284→---
   285→
   286→### Test 1.9: Loads all collections listing
   287→
   288→**Preconditions:** Seeded collections
   289→
   290→**Steps:**
   291→1. Navigate to `/collections`
   292→
   293→**Expected Results:**
   294→- Page displays "Collections"
   295→- No JavaScript errors
   296→
   297→---
   298→
   299→### Test 1.10: Has no errors on critical pages (batch)
   300→
   301→**Preconditions:** All seed data
   302→
   303→**Steps:**
   304→1. Visit all critical pages in batch: `/`, `/collections/new-arrivals`, `/products/classic-cotton-t-shirt`, `/cart`, `/account/login`, `/admin/login`, `/pages/about`, `/search?q=shirt`
   305→
   306→**Expected Results:**
   307→- No JavaScript errors on any of the visited pages
   308→
   309→**Total: 10 tests**
   310→
   311→---
   312→
   313→## Suite 2: Admin Authentication
   314→
   315→**File:** `tests/Browser/Admin/AuthenticationTest.php`
   316→
   317→**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.
   318→
   319→### Authentication Flow
   320→
   321→```mermaid
   322→stateDiagram-v2
   323→    [*] --> LoginPage: visit /admin/login
   324→    LoginPage --> Dashboard: valid credentials + click "Sign in"
   325→    LoginPage --> LoginPage: invalid credentials (show error)
   326→    LoginPage --> LoginPage: empty fields (show validation)
   327→    Dashboard --> Products: click "Products"
   328→    Dashboard --> Orders: click "Orders"
   329→    Dashboard --> Customers: click "Customers"
   330→    Dashboard --> Discounts: click "Discounts"
   331→    Dashboard --> Settings: click "Settings"
   332→    Dashboard --> Analytics: click "Analytics"
   333→    Dashboard --> Themes: click "Themes"
   334→    Dashboard --> LoginPage: click profile dropdown then "Logout"
   335→```
   336→
   337→---
   338→
   339→### Test 2.1: Can log in as admin
   340→
   341→**Preconditions:** Seeded admin user
   342→
   343→**Steps:**
   344→1. Navigate to `/admin/login`
   345→2. Enter email: `admin@acme.test`
   346→3. Enter password: `password`
   347→4. Click the "Sign in" button
   348→
   349→**Expected Results:**
   350→- Page displays "Dashboard"
   351→- No JavaScript errors
   352→
   353→---
   354→
   355→### Test 2.2: Shows error for invalid credentials
   356→
   357→**Preconditions:** Seeded admin user
   358→
   359→**Steps:**
   360→1. Navigate to `/admin/login`
   361→2. Enter email: `admin@acme.test`
   362→3. Enter password: `wrongpassword`
   363→4. Click the "Sign in" button
   364→
   365→**Expected Results:**
   366→- Page displays "Invalid credentials"
   367→- No JavaScript errors
   368→
   369→---
   370→
   371→### Test 2.3: Shows error for empty email
   372→
   373→**Preconditions:** None
   374→
   375→**Steps:**
   376→1. Navigate to `/admin/login`
   377→2. Enter password: `password` (leave email empty)
   378→3. Click the "Sign in" button
   379→
   380→**Expected Results:**
   381→- Validation error referencing "email" is visible
   382→- No JavaScript errors
   383→
   384→---
   385→
   386→### Test 2.4: Shows error for empty password
   387→
   388→**Preconditions:** None
   389→
   390→**Steps:**
   391→1. Navigate to `/admin/login`
   392→2. Enter email: `admin@acme.test` (leave password empty)
   393→3. Click the "Sign in" button
   394→
   395→**Expected Results:**
   396→- Validation error referencing "password" is visible
   397→- No JavaScript errors
   398→
   399→---
   400→
   401→### Test 2.5: Redirects unauthenticated users to login from dashboard
   402→
   403→**Preconditions:** Not logged in
   404→
   405→**Steps:**
   406→1. Navigate to `/admin` (without logging in)
   407→
   408→**Expected Results:**
   409→- Page displays "Sign in" (redirected to login form)
   410→- No JavaScript errors
   411→
   412→---
   413→
   414→### Test 2.6: Redirects unauthenticated users to login from products
   415→
   416→**Preconditions:** Not logged in
   417→
   418→**Steps:**
   419→1. Navigate to `/admin/products` (without logging in)
   420→
   421→**Expected Results:**
   422→- Page displays "Sign in" (redirected to login form)
   423→- No JavaScript errors
   424→
   425→---
   426→
   427→### Test 2.7: Can log out
   428→
   429→**Preconditions:** Seeded admin user
   430→
   431→**Steps:**
   432→1. Navigate to `/admin/login`
   433→2. Enter email: `admin@acme.test`
   434→3. Enter password: `password`
   435→4. Click the "Sign in" button
   436→5. Verify "Dashboard" is visible
   437→6. Click on "Admin User" (profile/user menu)
   438→7. Click "Logout"
   439→
   440→**Expected Results:**
   441→- Page displays "Sign in" (returned to login form)
   442→
   443→---
   444→
   445→### Test 2.8: Can navigate through admin sidebar sections
   446→
   447→**Preconditions:** Seeded admin user
   448→
   449→**Steps:**
   450→1. Navigate to `/admin/login`
   451→2. Enter email: `admin@acme.test`
   452→3. Enter password: `password`
   453→4. Click the "Sign in" button
   454→5. Click "Products" in the sidebar
   455→6. Verify "Products" heading is visible
   456→7. Click "Orders" in the sidebar
   457→8. Verify "Orders" heading is visible
   458→9. Click "Customers" in the sidebar
   459→10. Verify "Customers" heading is visible
   460→11. Click "Discounts" in the sidebar
   461→12. Verify "Discounts" heading is visible
   462→13. Click "Settings" in the sidebar
   463→14. Verify "Store Settings" heading is visible
   464→
   465→**Expected Results:**
   466→- Each section loads without JavaScript errors
   467→- Each section heading is visible after clicking its sidebar link
   468→
   469→---
   470→
   471→### Test 2.9: Can navigate to analytics from sidebar
   472→
   473→**Preconditions:** Seeded admin user
   474→
   475→**Steps:**
   476→1. Navigate to `/admin/login`
   477→2. Enter email: `admin@acme.test`
   478→3. Enter password: `password`
   479→4. Click the "Sign in" button
   480→5. Click "Analytics" in the sidebar
   481→
   482→**Expected Results:**
   483→- Page displays "Analytics"
   484→- No JavaScript errors
   485→
   486→---
   487→
   488→### Test 2.10: Can navigate to themes from sidebar
   489→
   490→**Preconditions:** Seeded admin user
   491→
   492→**Steps:**
   493→1. Navigate to `/admin/login`
   494→2. Enter email: `admin@acme.test`
   495→3. Enter password: `password`
   496→4. Click the "Sign in" button
   497→5. Click "Themes" in the sidebar
   498→
   499→**Expected Results:**

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

**Preconditions:** Seeded pages

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

**Expected Results:**
- Page displays "About"
- No JavaScript errors

---

### Test 1.8: Loads the search page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/search?q=shirt`

**Expected Results:**
- Page displays "shirt"
- No JavaScript errors

---

### Test 1.9: Loads all collections listing

**Preconditions:** Seeded collections

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

**Expected Results:**
- Page displays "Collections"
- No JavaScript errors

---

### Test 1.10: Has no errors on critical pages (batch)

**Preconditions:** All seed data

**Steps:**
1. Visit all critical pages in batch: `/`, `/collections/new-arrivals`, `/products/classic-cotton-t-shirt`, `/cart`, `/account/login`, `/admin/login`, `/pages/about`, `/search?q=shirt`

**Expected Results:**
- No JavaScript errors on any of the visited pages

**Total: 10 tests**

---

## Suite 2: Admin Authentication

**File:** `tests/Browser/Admin/AuthenticationTest.php`

**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.

### Authentication Flow

```mermaid
stateDiagram-v2
    [*] --> LoginPage: visit /admin/login
    LoginPage --> Dashboard: valid credentials + click "Sign in"
    LoginPage --> LoginPage: invalid credentials (show error)
    LoginPage --> LoginPage: empty fields (show validation)
    Dashboard --> Products: click "Products"
    Dashboard --> Orders: click "Orders"
    Dashboard --> Customers: click "Customers"
    Dashboard --> Discounts: click "Discounts"
    Dashboard --> Settings: click "Settings"
    Dashboard --> Analytics: click "Analytics"
    Dashboard --> Themes: click "Themes"
    Dashboard --> LoginPage: click profile dropdown then "Logout"
```

---

### Test 2.1: Can log in as admin

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button

**Expected Results:**
- Page displays "Dashboard"
- No JavaScript errors

---

### Test 2.2: Shows error for invalid credentials

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `wrongpassword`
4. Click the "Sign in" button

**Expected Results:**
- Page displays "Invalid credentials"
- No JavaScript errors

---

### Test 2.3: Shows error for empty email

**Preconditions:** None

**Steps:**
1. Navigate to `/admin/login`
2. Enter password: `password` (leave email empty)
3. Click the "Sign in" button

**Expected Results:**
- Validation error referencing "email" is visible
- No JavaScript errors

---

### Test 2.4: Shows error for empty password

**Preconditions:** None

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test` (leave password empty)
3. Click the "Sign in" button

**Expected Results:**
- Validation error referencing "password" is visible
- No JavaScript errors

---

### Test 2.5: Redirects unauthenticated users to login from dashboard

**Preconditions:** Not logged in

**Steps:**
1. Navigate to `/admin` (without logging in)

**Expected Results:**
- Page displays "Sign in" (redirected to login form)
- No JavaScript errors

---

### Test 2.6: Redirects unauthenticated users to login from products

**Preconditions:** Not logged in

**Steps:**
1. Navigate to `/admin/products` (without logging in)

**Expected Results:**
- Page displays "Sign in" (redirected to login form)
- No JavaScript errors

---

### Test 2.7: Can log out

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Verify "Dashboard" is visible
6. Click on "Admin User" (profile/user menu)
7. Click "Logout"

**Expected Results:**
- Page displays "Sign in" (returned to login form)

---

### Test 2.8: Can navigate through admin sidebar sections

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Products" in the sidebar
6. Verify "Products" heading is visible
7. Click "Orders" in the sidebar
8. Verify "Orders" heading is visible
9. Click "Customers" in the sidebar
10. Verify "Customers" heading is visible
11. Click "Discounts" in the sidebar
12. Verify "Discounts" heading is visible
13. Click "Settings" in the sidebar
14. Verify "Store Settings" heading is visible

**Expected Results:**
- Each section loads without JavaScript errors
- Each section heading is visible after clicking its sidebar link

---

### Test 2.9: Can navigate to analytics from sidebar

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Analytics" in the sidebar

**Expected Results:**
- Page displays "Analytics"
- No JavaScript errors

---

### Test 2.10: Can navigate to themes from sidebar

**Preconditions:** Seeded admin user

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Themes" in the sidebar

**Expected Results:**


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

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


Now I have a comprehensive understanding of all the spec files. Let me read the Phase 5 section of the implementation roadmap which specifically covers payments and orders.


Let me read the Phase 5 payment/order section and the checkout E2E tests.


   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_pending` | `beginPayment(Checkout)` | All data valid, reserve inventory |
   576→| `payment_pending` -> `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→| `PaymentProvider` | `Stripe`, `Paypal` |
   650→| `PaymentStatus` | `RequiresAction`, `Authorized`, `Captured`, `Failed`, `Refunded` |
   651→| `RefundStatus` | `Pending`, `Processed`, `Failed` |
   652→| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |
   653→
   654→#### Step 5.3: Payment Service
   655→
   656→**Files to create:**
   657→
   658→| Type | Name | Purpose |
   659→|------|------|---------|
   660→| Interface | `App\Contracts\PaymentProvider` | Defines createSession, handleWebhook, refund methods |
   661→| Service | `App\Services\Payments\StripePaymentProvider` | Implements PaymentProvider with Stripe Checkout Sessions in test mode |
   662→| Service | `App\Services\Payments\PaypalPaymentProvider` | Stub implementation that throws NotImplementedException |
   663→
   664→**PaymentProvider interface methods:**
   665→
   666→| Method | Description |
   667→|--------|-------------|
   668→| `createSession(Checkout $checkout, int $amount, string $currency): PaymentSession` | Creates payment session with the provider |
   669→| `handleWebhook(Request $request): void` | Processes incoming webhook from provider |
   670→| `refund(Payment $payment, int $amount): RefundResult` | Issues a refund through the provider |
   671→
   672→**StripePaymentProvider requirements:**
   673→- Creates Stripe Checkout Sessions in test mode
   674→- Webhook handler with HMAC signature verification
   675→- Idempotent event processing (checks `provider_payment_id` uniqueness)
   676→
   677→**Registration:** Bind `PaymentProvider` interface to `StripePaymentProvider` in `AppServiceProvider`.
   678→
   679→#### Step 5.4: Order Service
   680→
   681→**File to create:** `App\Services\OrderService`
   682→
   683→| Method | Description |
   684→|--------|-------------|
   685→| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
   686→| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
   687→| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |
   688→
   689→Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.
   690→
   691→#### Step 5.5: Refund Service
   692→
   693→**File to create:** `App\Services\RefundService`
   694→
   695→| Method | Description |
   696→|--------|-------------|
   697→| `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 |
   698→
   699→- Partial refunds: `financial_status` -> `partially_refunded`
   700→- Full refunds: `financial_status` -> `refunded`
   701→- Restock flag: calls `InventoryService::restock()` for each line
   702→
   703→#### Step 5.6: Fulfillment Service
   704→
   705→**File to create:** `App\Services\FulfillmentService`
   706→
   707→| Method | Description |
   708→|--------|-------------|
   709→| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities |
   710→| `ship(Fulfillment $fulfillment, array $tracking): void` | Sets tracking info, transitions to `shipped` |
   711→| `deliver(Fulfillment $fulfillment): void` | Transitions to `delivered` |
   712→
   713→Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).
   714→
   715→**Events to create:**
   716→
   717→| Type | Name | Purpose |
   718→|------|------|---------|
   719→| Event | `OrderCreated` | Dispatched when order is created from checkout |
   720→| Event | `OrderPaid` | Dispatched when payment is captured |
   721→| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
   722→| Event | `OrderCancelled` | Dispatched when order is cancelled |
   723→| Event | `OrderRefunded` | Dispatched when refund is processed |
   724→
   725→---
   726→
   727→### Phase 6: Customer Accounts
   728→
   729→**Priority:** MEDIUM - enhances the shopping experience.
   730→
   731→#### Step 6.1: Customer Auth
   732→
   733→- `Customer` model with custom `customer` guard (already configured in Phase 1)
   734→- Custom `CustomerUserProvider` - scopes queries by `store_id`
   735→- Customer email unique per store, not globally
   736→
   737→#### Step 6.2: Customer Account Pages (Livewire)
   738→
   739→**Files to create:**
   740→
   741→| Component | Route | Description |
   742→|-----------|-------|-------------|
   743→| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
   744→| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
   745→| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
   746→| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
   747→| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
   748→| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |
   749→
   750→Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
   751→
   752→---
   753→
   754→### Phase 7: Admin Panel
   755→
   756→**Priority:** MEDIUM - merchant management interface.
   757→
   758→#### Step 7.1: Admin Layout
   759→
   760→**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
   761→
   762→**Components to create:**
   763→
   764→| Component | Description |
   765→|-----------|-------------|
   766→| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
   767→| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
   768→| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |
   769→
   770→Toast notifications via Livewire events.
   771→
   772→#### Step 7.2: Dashboard
   773→
   774→**File to create:** `Admin\Dashboard` Livewire component
   775→
   776→Requirements:
   777→- KPI tiles: total sales (period), order count, average order value, conversion rate
   778→- Orders chart (last 30 days line chart)
   779→- Recent orders table (last 10)
   780→- Date range filtering via Livewire properties
   781→
   782→#### Step 7.3: Product Management
   783→
   784→**Files to create:**
   785→
   786→| Component | Description |
   787→|-----------|-------------|
   788→| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
   789→| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |
   790→
   791→> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.
   792→
   793→Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.
   794→
   795→Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
   796→
   797→#### Step 7.4: Order Management
   798→
   799→**Files to create:**

| `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_pending` | `beginPayment(Checkout)` | All data valid, reserve inventory |
| `payment_pending` -> `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` |
| `PaymentProvider` | `Stripe`, `Paypal` |
| `PaymentStatus` | `RequiresAction`, `Authorized`, `Captured`, `Failed`, `Refunded` |
| `RefundStatus` | `Pending`, `Processed`, `Failed` |
| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |

#### Step 5.3: Payment Service

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Interface | `App\Contracts\PaymentProvider` | Defines createSession, handleWebhook, refund methods |
| Service | `App\Services\Payments\StripePaymentProvider` | Implements PaymentProvider with Stripe Checkout Sessions in test mode |
| Service | `App\Services\Payments\PaypalPaymentProvider` | Stub implementation that throws NotImplementedException |

**PaymentProvider interface methods:**

| Method | Description |
|--------|-------------|
| `createSession(Checkout $checkout, int $amount, string $currency): PaymentSession` | Creates payment session with the provider |
| `handleWebhook(Request $request): void` | Processes incoming webhook from provider |
| `refund(Payment $payment, int $amount): RefundResult` | Issues a refund through the provider |

**StripePaymentProvider requirements:**
- Creates Stripe Checkout Sessions in test mode
- Webhook handler with HMAC signature verification
- Idempotent event processing (checks `provider_payment_id` uniqueness)

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

#### 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 |
| `ship(Fulfillment $fulfillment, array $tracking): void` | Sets tracking info, transitions to `shipped` |
| `deliver(Fulfillment $fulfillment): void` | Transitions to `delivered` |

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

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

#### Step 7.3: Product Management

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |

> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.

Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.

Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.

#### Step 7.4: Order Management

**Files to create:**


   500→- Page displays "Themes"
   501→- No JavaScript errors
   502→
   503→**Login Helper Pattern:** Tests 2.7-2.10 require the admin to be logged in first. Each test should perform the full login sequence (visit login page, fill credentials, click sign in, assert dashboard) before proceeding. This ensures test independence.
   504→
   505→**Total: 10 tests**
   506→
   507→---
   508→
   509→## Suite 3: Admin Product Management
   510→
   511→**File:** `tests/Browser/Admin/ProductManagementTest.php`
   512→
   513→**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.
   514→
   515→### Product Lifecycle States
   516→
   517→```mermaid
   518→stateDiagram-v2
   519→    [*] --> Draft: create product (default)
   520→    Draft --> Active: set status to Active + Save
   521→    Active --> Archived: set status to Archived + Save
   522→    Draft --> Archived: set status to Archived + Save
   523→    Archived --> Active: set status to Active + Save
   524→    Active --> Draft: set status to Draft + Save
   525→```
   526→
   527→---
   528→
   529→### Test 3.1: Shows the product list with seeded products
   530→
   531→**Preconditions:** Logged in as admin, seeded products
   532→
   533→**Steps:**
   534→1. Navigate to `/admin/login`
   535→2. Enter email: `admin@acme.test`
   536→3. Enter password: `password`
   537→4. Click the "Sign in" button
   538→5. Click "Products" in the sidebar
   539→
   540→**Expected Results:**
   541→- Page displays "Classic Cotton T-Shirt"
   542→- Page displays "Premium Slim Fit Jeans"
   543→- No JavaScript errors
   544→
   545→---
   546→
   547→### Test 3.2: Can create a new product
   548→
   549→**Preconditions:** Logged in as admin
   550→
   551→**Steps:**
   552→1. Navigate to `/admin/login`
   553→2. Log in as admin (email: `admin@acme.test`, password: `password`)
   554→3. Click "Products" in the sidebar
   555→4. Click "Add product"
   556→5. Enter title: `Test Product Created by E2E`
   557→6. Enter description: `This product was created by the E2E test suite.`
   558→7. Enter vendor: `Test Vendor`
   559→8. Enter product type: `T-Shirts`
   560→9. Enter variant price (variants.0.price): `29.99`
   561→10. Enter variant SKU (variants.0.sku): `E2E-TEST-001`
   562→11. Enter variant quantity (variants.0.quantity): `50`
   563→12. Click "Save"
   564→13. Click "Products" to return to the product list
   565→
   566→**Expected Results:**
   567→- After step 12: page displays "Product saved", no JavaScript errors
   568→- After step 13: page displays "Test Product Created by E2E" in the list
   569→
   570→---
   571→
   572→### Test 3.3: Can edit an existing product title
   573→
   574→**Preconditions:** Logged in as admin, seeded products
   575→
   576→**Steps:**
   577→1. Navigate to `/admin/login`
   578→2. Log in as admin
   579→3. Click "Products" in the sidebar
   580→4. Verify "Classic Cotton T-Shirt" is visible in the list
   581→5. Click "Classic Cotton T-Shirt" to open the edit form
   582→6. Clear the title field and enter: `Classic Cotton T-Shirt Updated`
   583→7. Click "Save"
   584→8. Click "Products" to return to the product list
   585→
   586→**Expected Results:**
   587→- After step 7: page displays "Product saved", no JavaScript errors
   588→- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list
   589→
   590→---
   591→
   592→### Test 3.4: Can archive a product
   593→
   594→**Preconditions:** Logged in as admin
   595→
   596→**Steps:**
   597→1. Navigate to `/admin/login`
   598→2. Log in as admin
   599→3. Click "Products" in the sidebar
   600→4. Click "Add product"
   601→5. Enter title: `Product To Archive`
   602→6. Enter variant price: `19.99`
   603→7. Enter variant SKU: `E2E-ARCHIVE-001`
   604→8. Enter variant quantity: `10`
   605→9. Click "Save"
   606→10. Verify "Product saved" is displayed
   607→11. Click "Products" to return to the product list
   608→12. Click "Product To Archive" to open the edit form
   609→13. Change status to "Archived" (click the Archived option)
   610→14. Click "Save"
   611→15. Click "Products" to return to the product list
   612→
   613→**Expected Results:**
   614→- After step 9: page displays "Product saved"
   615→- After step 14: page displays "Product saved"
   616→- After step 15: with default "Active" filter, the archived product should not appear in the list
   617→
   618→---
   619→
   620→### Test 3.5: Shows draft products only in admin, not storefront
   621→
   622→**Preconditions:** Logged in as admin, Product #15 exists with draft status
   623→
   624→**Steps:**
   625→1. Navigate to `/admin/login`
   626→2. Log in as admin
   627→3. Click "Products" in the sidebar
   628→4. Verify Product #15 is visible in the admin list with a "Draft" badge
   629→5. Open a new browser context / navigate to `/collections/t-shirts` (as guest)
   630→6. Verify the draft product does NOT appear in the collection listing
   631→7. Navigate to `/search?q=draft`
   632→8. Verify the draft product does NOT appear in search results
   633→
   634→**Expected Results:**
   635→- Step 4: draft product visible in admin, no JavaScript errors
   636→- Step 6: draft product absent from storefront collection, no JavaScript errors
   637→- Step 8: draft product absent from search results, no JavaScript errors
   638→
   639→---
   640→
   641→### Test 3.6: Can search products in admin
   642→
   643→**Preconditions:** Logged in as admin, seeded products
   644→
   645→**Steps:**
   646→1. Navigate to `/admin/login`
   647→2. Log in as admin
   648→3. Click "Products" in the sidebar
   649→4. Enter "Cotton" in the search input field
   650→
   651→**Expected Results:**
   652→- Page displays "Classic Cotton T-Shirt" in filtered results
   653→- No JavaScript errors
   654→
   655→---
   656→
   657→### Test 3.7: Can filter products by status in admin
   658→
   659→**Preconditions:** Logged in as admin, seeded products including drafts
   660→
   661→**Steps:**
   662→1. Navigate to `/admin/login`
   663→2. Log in as admin
   664→3. Click "Products" in the sidebar
   665→4. Click the "Draft" status filter tab
   666→5. Verify only draft products are visible
   667→6. Click the "Active" status filter tab
   668→
   669→**Expected Results:**
   670→- After step 4: only draft products are visible, no JavaScript errors
   671→- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors
   672→
   673→**Total: 7 tests**
   674→
   675→---
   676→
   677→## Suite 4: Admin Order Management
   678→
   679→**File:** `tests/Browser/Admin/OrderManagementTest.php`
   680→
   681→**Purpose:** Verify order listing, status filtering, order detail with timeline, fulfillment creation, and refund processing.
   682→
   683→### Order State Transitions (via admin actions)
   684→
   685→```mermaid
   686→stateDiagram-v2
   687→    state financial {
   688→        pending_f --> paid: payment captured
   689→        paid --> partially_refunded: partial refund
   690→        paid --> refunded: full refund
   691→    }
   692→    state fulfillment {
   693→        unfulfilled --> partial: partial fulfillment
   694→        unfulfilled --> fulfilled: full fulfillment
   695→        partial --> fulfilled: remaining items fulfilled
   696→    }
   697→```
   698→
   699→---
   700→
   701→### Test 4.1: Shows the order list with seeded orders
   702→
   703→**Preconditions:** Logged in as admin, seeded orders
   704→
   705→**Steps:**
   706→1. Navigate to `/admin/login`
   707→2. Log in as admin
   708→3. Click "Orders" in the sidebar
   709→
   710→**Expected Results:**
   711→- Page displays "#1001"
   712→- No JavaScript errors
   713→
   714→---
   715→
   716→### Test 4.2: Can filter orders by status
   717→
   718→**Preconditions:** Logged in as admin, seeded orders
   719→
   720→**Steps:**
   721→1. Navigate to `/admin/login`
   722→2. Log in as admin
   723→3. Click "Orders" in the sidebar
   724→4. Click the "Paid" status filter/tab
   725→5. Verify "#1001" is visible
   726→6. Click the "Fulfilled" status filter/tab
   727→7. Click the "All" tab to reset the filter
   728→
   729→**Expected Results:**
   730→- After step 4: "#1001" is visible, no JavaScript errors
   731→- After step 6: filtered results shown, no JavaScript errors
   732→- After step 7: full list shown, no JavaScript errors
   733→
   734→---
   735→
   736→### Test 4.3: Shows order detail with line items and totals
   737→
   738→**Preconditions:** Logged in as admin, Order #1001 exists
   739→
   740→**Steps:**
   741→1. Navigate to `/admin/login`
   742→2. Log in as admin
   743→3. Click "Orders" in the sidebar
   744→4. Click "#1001" to open the order detail
   745→
   746→**Expected Results:**
   747→- Page displays "#1001"
   748→- Page displays "Paid" (financial status)
   749→- Page displays "Unfulfilled" (fulfillment status)
   750→- Page displays "Classic Cotton T-Shirt" (line item)
   751→- Page displays "Subtotal", "Shipping", "Tax", and "Total" sections
   752→- No JavaScript errors
   753→
   754→---
   755→
   756→### Test 4.4: Shows order timeline events
   757→
   758→**Preconditions:** Logged in as admin, Order #1001 exists
   759→
   760→**Steps:**
   761→1. Navigate to `/admin/login`
   762→2. Log in as admin
   763→3. Click "Orders" in the sidebar
   764→4. Click "#1001" to open the order detail
   765→
   766→**Expected Results:**
   767→- Page displays "Timeline" section
   768→- Timeline shows at least the order creation event
   769→- No JavaScript errors
   770→
   771→---
   772→
   773→### Test 4.5: Can create a fulfillment
   774→
   775→**Preconditions:** Logged in as admin, Order #1001 is unfulfilled
   776→
   777→**Steps:**
   778→1. Navigate to `/admin/login`
   779→2. Log in as admin
   780→3. Click "Orders" in the sidebar
   781→4. Click "#1001" to open the order detail
   782→5. Click "Create fulfillment"
   783→6. Enter tracking company: `DHL`
   784→7. Enter tracking number: `DHL123456789`
   785→8. Click "Fulfill items"
   786→
   787→**Expected Results:**
   788→- Page displays "Fulfillment created"
   789→- Page displays "DHL" and "DHL123456789" in the fulfillment section
   790→- No JavaScript errors
   791→
   792→---
   793→
   794→### Test 4.6: Can process a refund
   795→
   796→**Preconditions:** Logged in as admin, Order #1001 is paid
   797→
   798→**Steps:**
   799→1. Navigate to `/admin/login`

- Page displays "Themes"
- No JavaScript errors

**Login Helper Pattern:** Tests 2.7-2.10 require the admin to be logged in first. Each test should perform the full login sequence (visit login page, fill credentials, click sign in, assert dashboard) before proceeding. This ensures test independence.

**Total: 10 tests**

---

## Suite 3: Admin Product Management

**File:** `tests/Browser/Admin/ProductManagementTest.php`

**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.

### Product Lifecycle States

```mermaid
stateDiagram-v2
    [*] --> Draft: create product (default)
    Draft --> Active: set status to Active + Save
    Active --> Archived: set status to Archived + Save
    Draft --> Archived: set status to Archived + Save
    Archived --> Active: set status to Active + Save
    Active --> Draft: set status to Draft + Save
```

---

### Test 3.1: Shows the product list with seeded products

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Products" in the sidebar

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "Premium Slim Fit Jeans"
- No JavaScript errors

---

### Test 3.2: Can create a new product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin (email: `admin@acme.test`, password: `password`)
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Test Product Created by E2E`
6. Enter description: `This product was created by the E2E test suite.`
7. Enter vendor: `Test Vendor`
8. Enter product type: `T-Shirts`
9. Enter variant price (variants.0.price): `29.99`
10. Enter variant SKU (variants.0.sku): `E2E-TEST-001`
11. Enter variant quantity (variants.0.quantity): `50`
12. Click "Save"
13. Click "Products" to return to the product list

**Expected Results:**
- After step 12: page displays "Product saved", no JavaScript errors
- After step 13: page displays "Test Product Created by E2E" in the list

---

### Test 3.3: Can edit an existing product title

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify "Classic Cotton T-Shirt" is visible in the list
5. Click "Classic Cotton T-Shirt" to open the edit form
6. Clear the title field and enter: `Classic Cotton T-Shirt Updated`
7. Click "Save"
8. Click "Products" to return to the product list

**Expected Results:**
- After step 7: page displays "Product saved", no JavaScript errors
- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list

---

### Test 3.4: Can archive a product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Product To Archive`
6. Enter variant price: `19.99`
7. Enter variant SKU: `E2E-ARCHIVE-001`
8. Enter variant quantity: `10`
9. Click "Save"
10. Verify "Product saved" is displayed
11. Click "Products" to return to the product list
12. Click "Product To Archive" to open the edit form
13. Change status to "Archived" (click the Archived option)
14. Click "Save"
15. Click "Products" to return to the product list

**Expected Results:**
- After step 9: page displays "Product saved"
- After step 14: page displays "Product saved"
- After step 15: with default "Active" filter, the archived product should not appear in the list

---

### Test 3.5: Shows draft products only in admin, not storefront

**Preconditions:** Logged in as admin, Product #15 exists with draft status

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify Product #15 is visible in the admin list with a "Draft" badge
5. Open a new browser context / navigate to `/collections/t-shirts` (as guest)
6. Verify the draft product does NOT appear in the collection listing
7. Navigate to `/search?q=draft`
8. Verify the draft product does NOT appear in search results

**Expected Results:**
- Step 4: draft product visible in admin, no JavaScript errors
- Step 6: draft product absent from storefront collection, no JavaScript errors
- Step 8: draft product absent from search results, no JavaScript errors

---

### Test 3.6: Can search products in admin

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Enter "Cotton" in the search input field

**Expected Results:**
- Page displays "Classic Cotton T-Shirt" in filtered results
- No JavaScript errors

---

### Test 3.7: Can filter products by status in admin

**Preconditions:** Logged in as admin, seeded products including drafts

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click the "Draft" status filter tab
5. Verify only draft products are visible
6. Click the "Active" status filter tab

**Expected Results:**
- After step 4: only draft products are visible, no JavaScript errors
- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors

**Total: 7 tests**

---

## Suite 4: Admin Order Management

**File:** `tests/Browser/Admin/OrderManagementTest.php`

**Purpose:** Verify order listing, status filtering, order detail with timeline, fulfillment creation, and refund processing.

### Order State Transitions (via admin actions)

```mermaid
stateDiagram-v2
    state financial {
        pending_f --> paid: payment captured
        paid --> partially_refunded: partial refund
        paid --> refunded: full refund
    }
    state fulfillment {
        unfulfilled --> partial: partial fulfillment
        unfulfilled --> fulfilled: full fulfillment
        partial --> fulfilled: remaining items fulfilled
    }
```

---

### Test 4.1: Shows the order list with seeded orders

**Preconditions:** Logged in as admin, seeded orders

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar

**Expected Results:**
- Page displays "#1001"
- No JavaScript errors

---

### Test 4.2: Can filter orders by status

**Preconditions:** Logged in as admin, seeded orders

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click the "Paid" status filter/tab
5. Verify "#1001" is visible
6. Click the "Fulfilled" status filter/tab
7. Click the "All" tab to reset the filter

**Expected Results:**
- After step 4: "#1001" is visible, no JavaScript errors
- After step 6: filtered results shown, no JavaScript errors
- After step 7: full list shown, no JavaScript errors

---

### Test 4.3: Shows order detail with line items and totals

**Preconditions:** Logged in as admin, Order #1001 exists

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail

**Expected Results:**
- Page displays "#1001"
- Page displays "Paid" (financial status)
- Page displays "Unfulfilled" (fulfillment status)
- Page displays "Classic Cotton T-Shirt" (line item)
- Page displays "Subtotal", "Shipping", "Tax", and "Total" sections
- No JavaScript errors

---

### Test 4.4: Shows order timeline events

**Preconditions:** Logged in as admin, Order #1001 exists

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail

**Expected Results:**
- Page displays "Timeline" section
- Timeline shows at least the order creation event
- No JavaScript errors

---

### Test 4.5: Can create a fulfillment

**Preconditions:** Logged in as admin, Order #1001 is unfulfilled

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail
5. Click "Create fulfillment"
6. Enter tracking company: `DHL`
7. Enter tracking number: `DHL123456789`
8. Click "Fulfill items"

**Expected Results:**
- Page displays "Fulfillment created"
- Page displays "DHL" and "DHL123456789" in the fulfillment section
- No JavaScript errors

---

### Test 4.6: Can process a refund

**Preconditions:** Logged in as admin, Order #1001 is paid

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


Now I have a comprehensive understanding of all the spec files. Let me also check the checkout E2E test section.


Now I have a comprehensive understanding of all spec files. Let me start making the edits. I'll work through each file systematically, starting with the foundational changes and progressing to the dependent ones. Let me update the task tracking and begin editing.


Updated task #1 status


Now let me make all the edits. I'll start with the three core spec files in parallel.

**1. Database Schema (01)** - Add `payment_method` to checkouts/orders, change payments.provider CHECK:


Edit

| status | TEXT | No | 'started' | CHECK IN ('started', 'addressed', 'shipping_selected', 'payment_pending', 'completed', 'expired') | |
| email | TEXT | Yes | NULL | - | Contact email |

| status | TEXT | No | 'started' | CHECK IN ('started', 'addressed', 'shipping_selected', 'payment_pending', 'completed', 'expired') | |
| payment_method | TEXT | Yes | NULL | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Selected payment method |
| email | TEXT | Yes | NULL | - | Contact email |