   800→
   801→#### Step 7.3: Product Management
   802→
   803→**Files to create:**
   804→
   805→| Component | Description |
   806→|-----------|-------------|
   807→| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
   808→| `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. |
   809→
   810→> **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.
   811→
   812→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.
   813→
   814→Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
   815→
   816→#### Step 7.4: Order Management
   817→
   818→**Files to create:**
   819→
   820→| Component | Description |
   821→|-----------|-------------|
   822→| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
   823→| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |
   824→
   825→Fulfillment modal: select lines and quantities, add tracking info, submit.
   826→Refund modal: enter amount (partial or full), reason, restock checkbox, submit.
   827→
   828→#### Step 7.5: Other Admin Sections
   829→
   830→**Files to create:**
   831→
   832→| Component | Description |
   833→|-----------|-------------|
   834→| `Admin\Collections\Index` | Collection list |
   835→| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
   836→| `Admin\Customers\Index` | Customer list with search |
   837→| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
   838→| `Admin\Discounts\Index` | Discount list with status/type filters |
   839→| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
   840→| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
   841→| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
   842→| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
   843→| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
   844→| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
   845→| `Admin\Pages\Index` | Content pages list |
   846→| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
   847→| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
   848→| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
   849→| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
   850→| `Admin\Apps\Index` | App directory |
   851→| `Admin\Apps\Show` | Installed app detail |
   852→| `Admin\Developers\Index` | API token management, webhook subscriptions |
   853→
   854→---
   855→
   856→### Phase 8: Search
   857→
   858→**Priority:** LOW - enhances product discovery.
   859→
   860→#### Step 8.1: Migrations
   861→
   862→1. `create_search_settings_table` - PK: `store_id`
   863→2. `create_search_queries_table` - FK: `store_id`
   864→3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table
   865→
   866→#### Step 8.2: Search Service
   867→
   868→**File to create:** `App\Services\SearchService`
   869→
   870→| Method | Description |
   871→|--------|-------------|
   872→| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
   873→| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
   874→| `syncProduct(Product $product): void` | Upsert into FTS5 index |
   875→| `removeProduct(int $productId): void` | Delete from FTS5 index |
   876→
   877→**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.
   878→
   879→#### Step 8.3: Search UI
   880→
   881→**Files to create:**
   882→
   883→| Component | Description |
   884→|-----------|-------------|
   885→| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
   886→| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |
   887→
   888→---
   889→
   890→### Phase 9: Analytics
   891→
   892→**Priority:** LOW - reporting and insights.
   893→
   894→#### Step 9.1: Migrations
   895→
   896→1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
   897→2. `create_analytics_daily_table` - composite PK: `store_id` + `date`
   898→
   899→#### Step 9.2: Analytics Service
   900→
   901→**File to create:** `App\Services\AnalyticsService`
   902→
   903→| Method | Description |
   904→|--------|-------------|
   905→| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
   906→| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |
   907→
   908→Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.
   909→
   910→**File to create:** `App\Jobs\AggregateAnalytics`
   911→
   912→Requirements:
   913→- Runs daily via `routes/console.php`
   914→- Aggregates raw events into `analytics_daily` rows
   915→- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
   916→
   917→---
   918→
   919→### Phase 10: Apps and Webhooks
   920→
   921→**Priority:** LOW - extensibility.
   922→
   923→#### Step 10.1: Migrations
   924→
   925→1. `create_apps_table`
   926→2. `create_app_installations_table` - FK: `store_id`, `app_id`
   927→3. `create_oauth_clients_table` - FK: `app_id`
   928→4. `create_oauth_tokens_table` - FK: `installation_id`
   929→5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
   930→6. `create_webhook_deliveries_table` - FK: `subscription_id`
   931→
   932→#### Step 10.2: Webhook Service
   933→
   934→**File to create:** `App\Services\WebhookService`
   935→
   936→| Method | Description |
   937→|--------|-------------|
   938→| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
   939→| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
   940→| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |
   941→
   942→**File to create:** `App\Jobs\DeliverWebhook`
   943→
   944→Requirements:
   945→- HTTP POST to target URL with JSON payload
   946→- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
   947→- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
   948→- Records response in `webhook_deliveries`
   949→- Circuit breaker: after 5 consecutive failures, pause subscription
   950→
   951→---
   952→
   953→### Phase 11: Polish
   954→
   955→**Priority:** LOW but important for completeness.
   956→
   957→1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
   958→2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
   959→3. Dark mode: `dark:` prefix on all storefront views and admin views
   960→4. Error pages: styled 404 and 503 pages matching the storefront theme
   961→5. Structured logging: JSON channel in `config/logging.php`
   962→6. Seed data: comprehensive seeders for demo stores
   963→
   964→---
   965→
   966→### Phase 12: Full Test Suite Execution
   967→
   968→**Priority:** Runs after all phases are complete.
   969→
   970→**Verification steps:**
   971→
   972→1. Run the full unit and feature test suite and confirm all tests pass
   973→2. Run all browser tests and confirm they pass
   974→3. Run the code style formatter/checker and confirm conformance
   975→4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
   976→5. Manual smoke: visit storefront, navigate products, add to cart, checkout
   977→6. Manual smoke: visit admin login, authenticate, manage products and orders
   978→
   979→---
   980→
   981→## 3. Pest Unit/Feature Test Specification
   982→
   983→All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.
   984→
   985→### Test Directory Structure
   986→
   987→| Directory | Files |
   988→|-----------|-------|
   989→| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
   990→| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
   991→| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
   992→| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
   993→| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
   994→| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
   995→| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
   996→| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
   997→| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
   998→| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
   999→| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |

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

| Component | Description |
|-----------|-------------|
| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |

Fulfillment modal: select lines and quantities, add tracking info, submit.
Refund modal: enter amount (partial or full), reason, restock checkbox, submit.

#### Step 7.5: Other Admin Sections

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Collections\Index` | Collection list |
| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
| `Admin\Customers\Index` | Customer list with search |
| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
| `Admin\Discounts\Index` | Discount list with status/type filters |
| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
| `Admin\Pages\Index` | Content pages list |
| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
| `Admin\Apps\Index` | App directory |
| `Admin\Apps\Show` | Installed app detail |
| `Admin\Developers\Index` | API token management, webhook subscriptions |

---

### Phase 8: Search

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

#### Step 8.1: Migrations

1. `create_search_settings_table` - PK: `store_id`
2. `create_search_queries_table` - FK: `store_id`
3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table

#### Step 8.2: Search Service

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

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

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

#### Step 8.3: Search UI

**Files to create:**

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

---

### Phase 9: Analytics

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

#### Step 9.1: Migrations

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

#### Step 9.2: Analytics Service

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

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

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

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

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

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

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

#### Step 10.2: Webhook Service

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

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

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

Requirements:
- HTTP POST to target URL with JSON payload
- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
- Records response in `webhook_deliveries`
- Circuit breaker: after 5 consecutive failures, pause subscription

---

### Phase 11: Polish

**Priority:** LOW but important for completeness.

1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
3. Dark mode: `dark:` prefix on all storefront views and admin views
4. Error pages: styled 404 and 503 pages matching the storefront theme
5. Structured logging: JSON channel in `config/logging.php`
6. Seed data: comprehensive seeders for demo stores

---

### Phase 12: Full Test Suite Execution

**Priority:** Runs after all phases are complete.

**Verification steps:**

1. Run the full unit and feature test suite and confirm all tests pass
2. Run all browser tests and confirm they pass
3. Run the code style formatter/checker and confirm conformance
4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
5. Manual smoke: visit storefront, navigate products, add to cart, checkout
6. Manual smoke: visit admin login, authenticate, manage products and orders

---

## 3. Pest Unit/Feature Test Specification

All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.

### Test Directory Structure

| Directory | Files |
|-----------|-------|
| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |


  1000→| `tests/Feature/Analytics/` | `EventIngestionTest.php`, `AggregationTest.php` |
  1001→| `tests/Feature/Webhooks/` | `WebhookDeliveryTest.php`, `WebhookSignatureTest.php` |
  1002→| `tests/Feature/Api/` | `StorefrontCartApiTest.php`, `StorefrontCheckoutApiTest.php`, `AdminProductApiTest.php`, `AdminOrderApiTest.php` |
  1003→| `tests/Browser/` | Covered in `08-PLAYWRIGHT-E2E-PLAN.md` |
  1004→
  1005→---
  1006→
  1007→### Unit Tests
  1008→
  1009→---
  1010→
  1011→#### tests/Unit/PricingEngineTest.php
  1012→
  1013→**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.
  1014→
  1015→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1016→|-----------|----------------------|----------------|----------|
  1017→| calculates subtotal from line items | 2 lines: variant A 2499 cents x 2, variant B 7999 cents x 1 | subtotal = 12997 | happy path |
  1018→| calculates subtotal for a single line | 1 line: 1500 cents x 3 | subtotal = 4500 | happy path |
  1019→| returns zero subtotal for empty cart | 0 lines | subtotal = 0 | edge case |
  1020→| applies percent discount correctly | subtotal 10000, discount 10% (value_amount = 10, value_type = percent) | discount = 1000, discounted subtotal = 9000 | happy path |
  1021→| applies fixed discount correctly | subtotal 10000, discount 500 cents fixed | discount = 500, discounted subtotal = 9500 | happy path |
  1022→| caps fixed discount at subtotal so it never goes negative | subtotal 300, discount 500 cents fixed | discount = 300, discounted subtotal = 0 | edge case |
  1023→| applies free shipping discount by zeroing shipping | subtotal 5000, shipping 499, discount type free_shipping | shipping = 0 | happy path |
  1024→| calculates tax exclusive correctly | net amount 10000, rate 1900 basis points (19%) | tax = 1900, gross = 11900 | happy path |
  1025→| extracts tax from inclusive price correctly | gross amount 11900, rate 1900 basis points (19%) | tax = 1900, net = 10000 | happy path |
  1026→| returns zero tax when rate is zero | amount 10000, rate 0 | tax = 0 | edge case |
  1027→| calculates shipping flat rate | rate config type=flat, amount=499 | shipping = 499 | happy path |
  1028→| calculates full checkout totals end to end | 2 lines x 2499 = 4998, discount WELCOME10 (10%), shipping flat 499, tax 19% inclusive; pipeline: subtotal 4998 -> discount 499 -> discounted 4499 -> shipping 499 -> tax extracted from gross | total calculation matches expected value | happy path |
  1029→| handles rounding correctly with odd cent amounts | 3 lines with prices that create fractional discount allocations | sum of rounded allocations equals total discount (no off-by-one) | edge case |
  1030→| produces identical results for identical inputs | Run calculation twice with same inputs | both PricingResult objects are identical | edge case |
  1031→
  1032→---
  1033→
  1034→#### tests/Unit/DiscountCalculatorTest.php
  1035→
  1036→**Setup:** Instantiate `DiscountService` with mock store and cart.
  1037→
  1038→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1039→|-----------|----------------------|----------------|----------|
  1040→| validates an active discount code | discount: status=active, starts_at=yesterday, ends_at=tomorrow | validation passes, returns Discount model | happy path |
  1041→| rejects an expired discount code | discount: status=active, ends_at=yesterday | throws InvalidDiscountException with reason 'expired' | validation |
  1042→| rejects a not-yet-active discount code | discount: status=active, starts_at=tomorrow | throws InvalidDiscountException with reason 'not_yet_active' | validation |
  1043→| rejects a discount that has reached its usage limit | discount: usage_limit=10, usage_count=10 | throws InvalidDiscountException with reason 'usage_limit_reached' | validation |
  1044→| rejects an unknown discount code | look up code 'DOESNOTEXIST' | throws InvalidDiscountException with reason 'not_found' | validation |
  1045→| performs case-insensitive code lookup | discount with code 'SUMMER20'; validate code 'summer20' | validation passes | edge case |
  1046→| enforces minimum purchase amount rule | discount with rules_json containing minimum_purchase: 5000; cart subtotal: 3000 | throws InvalidDiscountException with reason 'minimum_not_met' | validation |
  1047→| passes minimum purchase when cart meets threshold | discount with rules_json minimum_purchase: 5000; cart subtotal: 5000 | validation passes | happy path |
  1048→| calculates percent discount amount | discount: 15%, subtotal: 10000 | discount amount = 1500 | happy path |
  1049→| calculates fixed discount amount | discount: 500 cents fixed, subtotal: 10000 | discount amount = 500 | happy path |
  1050→| handles free shipping discount type | discount: type=free_shipping | discount amount = 0, free_shipping flag = true | happy path |
  1051→| allocates discount proportionally across multiple lines | 2 lines: line A subtotal 7500, line B subtotal 2500 (total 10000); discount: 10% = 1000 | line A allocation = 750, line B allocation = 250 | happy path |
  1052→| distributes rounding remainder to the last qualifying line | 3 lines with subtotals that create uneven division | sum of allocations exactly equals total discount | edge case |
  1053→
  1054→---
  1055→
  1056→#### tests/Unit/TaxCalculatorTest.php
  1057→
  1058→**Setup:** Instantiate `TaxCalculator` directly.
  1059→
  1060→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1061→|-----------|----------------------|----------------|----------|
  1062→| calculates manual tax exclusive | net amount: 10000, rate: 1900 (19%) | tax = 1900 | happy path |
  1063→| extracts manual tax from inclusive amount | gross amount: 11900, rate: 1900 (19%) | tax = 1900, net = 10000 | happy path |
  1064→| returns zero tax when no rate is configured | amount: 10000, rate: 0 | tax = 0 | edge case |
  1065→| handles zero amount lines | amount: 0, rate: 1900 | tax = 0 | edge case |
  1066→| calculates tax with non-standard rate | net amount: 8999, rate: 700 (7%) | tax = 629 (rounded) | happy path |
  1067→| extracts tax correctly for small amounts | gross amount: 119, rate: 1900 | tax = 19, net = 100 | edge case |
  1068→| handles high tax rates | net amount: 10000, rate: 2500 (25%) | tax = 2500 | happy path |
  1069→
  1070→---
  1071→
  1072→#### tests/Unit/ShippingCalculatorTest.php
  1073→
  1074→**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.
  1075→
  1076→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1077→|-----------|----------------------|----------------|----------|
  1078→| matches a zone by country code | zone countries_json: ["DE","AT","CH"]; address country: "DE" | zone matched | happy path |
  1079→| matches a zone by region code | zone regions_json: ["US-NY","US-CA"]; address: country "US", province_code "US-NY" | zone matched | happy path |
  1080→| returns empty when no zone matches the address | zone countries_json: ["DE"]; address country: "FR" | empty collection returned | edge case |
  1081→| calculates a flat rate | rate type: flat, config: amount 499 | shipping = 499 | happy path |
  1082→| calculates a weight-based rate | rate type: weight, config: ranges 0-500g=499, 501-2000g=899; cart total weight: 750g | shipping = 899 | happy path |
  1083→| calculates a price-based rate | rate type: price, config: ranges 0-5000=799, 5001-999999=399; cart subtotal: 7500 | shipping = 399 | happy path |
  1084→| returns zero shipping when no items require shipping | all cart line variants have requires_shipping = false | shipping = 0 | edge case |
  1085→| returns the correct rate when multiple zones match and the first is selected | two zones match, each with different rates | returns rates from both zones for customer selection | edge case |
  1086→| skips inactive rates | rate with is_active = 0 | rate not included in available rates | edge case |
  1087→
  1088→---
  1089→
  1090→#### tests/Unit/CartVersionTest.php
  1091→
  1092→**Setup:** Instantiate Cart model or CartService.
  1093→
  1094→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1095→|-----------|----------------------|----------------|----------|
  1096→| starts at version 1 | create new cart | cart_version = 1 | happy path |
  1097→| increments version on add line | add a line to cart | cart_version = 2 | happy path |
  1098→| increments version on update quantity | update line quantity | cart_version incremented by 1 | happy path |
  1099→| increments version on remove line | remove a line | cart_version incremented by 1 | happy path |
  1100→| detects version mismatch | cart at version 3, client sends expected_version 2 | CartVersionMismatchException thrown | validation |
  1101→
  1102→---
  1103→
  1104→#### tests/Unit/HandleGeneratorTest.php
  1105→
  1106→**Setup:** Instantiate `HandleGenerator` with mock database queries.
  1107→
  1108→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1109→|-----------|----------------------|----------------|----------|
  1110→| generates a slug from title | title: "My Amazing Product" | handle = "my-amazing-product" | happy path |
  1111→| appends suffix on collision | title: "T-Shirt" (handle "t-shirt" already exists) | handle = "t-shirt-1" | edge case |
  1112→| increments suffix on multiple collisions | "t-shirt" and "t-shirt-1" both exist | handle = "t-shirt-2" | edge case |
  1113→| handles special characters | title: "Loewe's Fall/Winter 2026" | handle is a valid slug (e.g., "loewes-fallwinter-2026") | edge case |
  1114→| excludes current record id from collision check | product #5 with handle "t-shirt", updating product #5 | handle = "t-shirt" (no suffix needed) | edge case |
  1115→| scopes uniqueness check to store | store 1 has "t-shirt", generating for store 2 | handle = "t-shirt" (no suffix needed) | edge case |
  1116→
  1117→---
  1118→
  1119→### Feature Tests
  1120→
  1121→All feature tests use the `RefreshDatabase` trait. Factory setup creates the necessary Store, User, and domain context.
  1122→
  1123→**Shared helper setup** (in `tests/Pest.php` or trait):
  1124→- `createStoreContext()`: creates Organization, Store, StoreDomain, User with Owner role, binds `current_store` in container
  1125→- `actingAsAdmin(User $user)`: authenticates as admin and sets store in session
  1126→- `actingAsCustomer(Customer $customer)`: authenticates as customer with customer guard
  1127→
  1128→---
  1129→
  1130→#### tests/Feature/Tenancy/TenantResolutionTest.php
  1131→
  1132→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1133→|-----------|----------------------|----------------|----------|
  1134→| resolves store from hostname for storefront requests | store with domain "acme-fashion.test"; GET "/" with Host header "acme-fashion.test" | 200 response, current_store bound in container | happy path |
  1135→| returns 404 for unknown hostname | GET "/" with Host header "nonexistent.test" | 404 response | validation |
  1136→| returns 503 for suspended store on storefront | store with status "suspended"; GET "/" with Host header for suspended store | 503 response | validation |
  1137→| resolves store from session for admin requests | login as admin, set current_store_id in session; GET "/admin" | 200 response, correct store in context | happy path |
  1138→| denies admin access when user has no store_users record | login as admin, session points to store user is not associated with; GET "/admin" | 403 response | authorization |
  1139→| caches hostname lookup | first request resolves from DB; second request resolves from cache (no additional query) | cache key "store_domain:{hostname}" exists | happy path |
  1140→
  1141→---
  1142→
  1143→#### tests/Feature/Tenancy/StoreIsolationTest.php
  1144→
  1145→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1146→|-----------|----------------------|----------------|----------|
  1147→| scopes product queries to the current store | store A has 3 products, store B has 5 products; set current_store to store A | Product::all() count = 3 | happy path |
  1148→| scopes order queries to the current store | store A has 2 orders, store B has 7 orders; set current_store to store A | Order::count() = 2 | happy path |
  1149→| automatically sets store_id on model creation | set current_store to store A; create a product without explicitly setting store_id | product->store_id = store A's id | happy path |
  1150→| prevents accessing another stores records via direct ID | store A product with id 1; set current_store to store B | Product::find(1) returns null | authorization |
  1151→| allows cross-store access when global scope is removed | remove StoreScope; query all products | count includes all stores | edge case |
  1152→
  1153→---
  1154→
  1155→#### tests/Feature/Auth/AdminAuthTest.php
  1156→
  1157→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1158→|-----------|----------------------|----------------|----------|
  1159→| renders the admin login page | GET /admin/login | 200 response, sees "Login" text | happy path |
  1160→| authenticates an admin user with valid credentials | POST /admin/login with valid email/password | redirected to /admin, session has auth | happy path |
  1161→| rejects invalid credentials | POST /admin/login with wrong password | redirected back, session has errors, sees "Invalid credentials" | validation |
  1162→| does not reveal whether email or password is incorrect | POST /admin/login with non-existent email | same generic "Invalid credentials" error | validation |
  1163→| rate limits login attempts | make 6 POST requests with wrong credentials within 1 minute | 6th request returns 429 Too Many Requests | validation |
  1164→| regenerates session on successful login | record session ID before login; login successfully | session ID changed | happy path |
  1165→| logs out and invalidates session | login, then POST /admin/logout | redirected to /admin/login, session invalidated, no longer authenticated | happy path |
  1166→| redirects unauthenticated users to login | GET /admin without auth | redirected to /admin/login | authorization |
  1167→| supports remember me functionality | login with remember=true | remember_web cookie is set | happy path |
  1168→| records last_login_at on successful login | login successfully | user.last_login_at is updated to now | happy path |
  1169→
  1170→---
  1171→
  1172→#### tests/Feature/Auth/CustomerAuthTest.php
  1173→
  1174→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1175→|-----------|----------------------|----------------|----------|
  1176→| renders the customer login page | GET /account/login on storefront domain | 200 response, sees login form | happy path |
  1177→| authenticates a customer with valid credentials | POST login with valid email/password | redirected to /account | happy path |
  1178→| rejects invalid customer credentials | POST login with wrong password | redirected back with errors | validation |
  1179→| scopes customer login to the current store | customer exists in store A but not store B; login on store B domain with store A customer credentials | login fails | authorization |
  1180→| rate limits customer login attempts | 6 attempts in 1 minute | 429 on 6th attempt | validation |
  1181→| registers a new customer | POST /account/register with name, email, password, password_confirmation | customer created in DB, auto-logged in, redirected to /account | happy path |
  1182→| rejects duplicate email registration in the same store | customer with email already exists in current store; POST /account/register with same email | validation error on email | validation |
  1183→| allows same email in different stores | customer with email in store A; register same email on store B | registration succeeds | edge case |
  1184→| logs out customer and redirects to login | login, then POST /account/logout | redirected to /account/login | happy path |
  1185→| merges guest cart into customer cart on login | guest has cart with variant A qty 2; customer has cart with variant A qty 1 and variant B qty 3; login | customer cart has variant A qty 3, variant B qty 3; guest cart abandoned | happy path |
  1186→
  1187→---
  1188→
  1189→#### tests/Feature/Auth/SanctumTokenTest.php
  1190→
  1191→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1192→|-----------|----------------------|----------------|----------|
  1193→| creates a personal access token with abilities | acting as admin, create token with abilities ['read-products', 'write-products'] | token row in personal_access_tokens, token string returned | happy path |
  1194→| authenticates API request with valid token | GET /api/admin/v1/stores/{storeId}/products with Bearer token | 200 response | happy path |
  1195→| rejects API request with invalid token | GET /api/admin/v1/stores/{storeId}/products with fake token | 401 response | validation |
  1196→| enforces token abilities | token has only 'read-products'; POST /api/admin/v1/stores/{storeId}/products (requires write-products) | 403 response | authorization |
  1197→| revokes a token | create token, then delete it; make API request with revoked token | 401 response | happy path |
  1198→
  1199→---
  1200→
  1201→#### tests/Feature/Products/ProductCrudTest.php
  1202→
  1203→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1204→|-----------|----------------------|----------------|----------|
  1205→| lists products for the current store | create 5 products for current store; GET /admin/products | 200 response, sees all 5 product titles | happy path |
  1206→| creates a product with a default variant | POST via Livewire action: title, description, status=draft | product in DB, one variant with is_default=1, inventory_item created | happy path |
  1207→| generates a unique handle from the title | create product with title "Summer T-Shirt" | handle = "summer-t-shirt" | happy path |
  1208→| appends suffix when handle collides | create two products with title "T-Shirt" | handles are "t-shirt" and "t-shirt-1" | edge case |
  1209→| updates a product | create product, then update title and description | DB reflects changes | happy path |
  1210→| transitions product from draft to active | product in draft with valid variant; transition to active | status = active, published_at set | happy path |
  1211→| rejects draft to active without a priced variant | product in draft, variant price = 0; transition to active | exception thrown, status unchanged | validation |
  1212→| transitions product from active to archived | active product; transition to archived | status = archived | happy path |
  1213→| prevents active to draft when order lines exist | active product with order references; transition to draft | exception thrown | validation |
  1214→| hard deletes a draft product with no order references | draft product, no orders; delete | product removed from DB | happy path |
  1215→| prevents deletion of product with order references | product with order_lines referencing its variants; attempt delete | exception thrown | validation |
  1216→| filters products by status | 3 active, 2 draft, 1 archived; filter by status=active | 3 results | happy path |
  1217→| searches products by title | product titled "Organic Cotton Hoodie"; search "cotton" | product found in results | happy path |
  1218→
  1219→---
  1220→
  1221→#### tests/Feature/Products/VariantTest.php
  1222→
  1223→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1224→|-----------|----------------------|----------------|----------|
  1225→| creates variants from option matrix | product with options: Size [S, M, L], Color [Red, Blue]; rebuild matrix | 6 variants created (3x2) | happy path |
  1226→| preserves existing variants when adding an option value | existing: Size [S, M] = 2 variants; add Size L; rebuild matrix | 3 variants, original 2 unchanged (prices preserved) | happy path |
  1227→| archives orphaned variants with order references | variant referenced in order_lines; remove its option value | variant status = archived, not deleted | edge case |
  1228→| deletes orphaned variants without order references | variant not referenced in any order; remove its option value | variant deleted from DB | happy path |
  1229→| auto-creates default variant for products without options | create product with no options | 1 variant with is_default=1 | happy path |
  1230→| validates SKU uniqueness within store | create variant with SKU "TSH-001" in store A; attempt to create another variant with same SKU in store A | validation error | validation |
  1231→| allows duplicate SKU across different stores | SKU "TSH-001" in store A; create variant with "TSH-001" in store B | success | edge case |
  1232→| allows null SKUs | create two variants with null SKU | both created successfully | edge case |
  1233→
  1234→---
  1235→
  1236→#### tests/Feature/Products/InventoryTest.php
  1237→
  1238→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1239→|-----------|----------------------|----------------|----------|
  1240→| creates inventory item when variant is created | create product variant | inventory_item exists with quantity_on_hand=0, quantity_reserved=0 | happy path |
  1241→| checks availability correctly | on_hand=10, reserved=3 | available = 7 | happy path |
  1242→| reserves inventory | on_hand=10, reserved=0; reserve 3 | reserved=3, available=7 | happy path |
  1243→| throws InsufficientInventoryException when reserving more than available with deny policy | on_hand=5, reserved=3, policy=deny; reserve 3 | InsufficientInventoryException thrown | validation |
  1244→| allows overselling with continue policy | on_hand=2, reserved=0, policy=continue; reserve 5 | reserved=5 (no exception) | edge case |
  1245→| releases reserved inventory | on_hand=10, reserved=5; release 3 | reserved=2 | happy path |
  1246→| commits inventory on order completion | on_hand=10, reserved=3; commit 3 | on_hand=7, reserved=0 | happy path |
  1247→| restocks inventory | on_hand=5; restock 10 | on_hand=15 | happy path |
  1248→
  1249→---
  1250→
  1251→#### tests/Feature/Products/CollectionTest.php
  1252→
  1253→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1254→|-----------|----------------------|----------------|----------|
  1255→| creates a collection with a unique handle | create collection titled "Summer Sale" | handle = "summer-sale", saved in DB | happy path |
  1256→| adds products to a collection | create collection, add 3 products | collection_products pivot has 3 rows | happy path |
  1257→| removes products from a collection | collection with 3 products, remove 1 | 2 remain | happy path |
  1258→| reorders products within a collection | 3 products with positions 0,1,2; reorder to 2,0,1 | positions updated correctly | happy path |
  1259→| transitions collection from draft to active | draft collection; transition to active | status = active | happy path |
  1260→| lists collections with product count | collection A with 5 products, collection B with 3 products; list collections | each collection shows correct product count | happy path |
  1261→| scopes collections to current store | store A has 2 collections, store B has 4; current store = A | Collection::count() = 2 | authorization |
  1262→
  1263→---
  1264→
  1265→#### tests/Feature/Products/MediaUploadTest.php
  1266→
  1267→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1268→|-----------|----------------------|----------------|----------|
  1269→| uploads an image for a product | POST Livewire file upload with a test image | product_media row created with status=processing | happy path |
  1270→| processes uploaded image and generates variants | dispatch ProcessMediaUpload job | media status = ready, file exists in storage | happy path |
  1271→| rejects non-image file types | attempt to upload a .txt file | validation error | validation |
  1272→| sets alt text on media | upload image, then update alt_text | alt_text persisted | happy path |
  1273→| reorders media positions | upload 3 images, reorder | position values updated | happy path |
  1274→| deletes media and removes file from storage | upload image, then delete | product_media row removed, file removed from disk | happy path |
  1275→
  1276→---
  1277→
  1278→#### tests/Feature/Cart/CartServiceTest.php
  1279→
  1280→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1281→|-----------|----------------------|----------------|----------|
  1282→| creates a cart for the current store | create cart | cart in DB with store_id, currency, version=1, status=active | happy path |
  1283→| adds a line item to the cart | active product with variant priced at 2500; add to cart qty 2 | cart_line created, unit_price=2500, subtotal=5000, total=5000 | happy path |
  1284→| increments quantity when adding an existing variant | cart already has variant A qty 1; add variant A qty 2 | single line with qty 3, subtotal=7500 | happy path |
  1285→| rejects add when product is not active | product status = draft; attempt to add variant to cart | exception thrown | validation |
  1286→| rejects add when inventory is insufficient and policy is deny | variant on_hand=2, policy=deny; attempt to add qty 5 | InsufficientInventoryException | validation |
  1287→| allows add when inventory is insufficient but policy is continue | variant on_hand=2, policy=continue; add qty 5 | line created with qty 5 | edge case |
  1288→| updates line quantity | cart with variant A qty 2; update to qty 5 | line quantity=5, amounts recalculated | happy path |
  1289→| removes a line when quantity set to zero | update line quantity to 0 | line removed from cart | edge case |
  1290→| removes a specific line item | cart with 2 lines, remove first | 1 line remains | happy path |
  1291→| increments cart version on every mutation | create cart (v1), add line (v2), update qty (v3), remove line (v4) | cart_version = 4 | happy path |
  1292→| returns cart via session for guest users | create cart, store ID in session; get cart for session | same cart returned | happy path |
  1293→| merges guest cart into customer cart on login | guest cart: variant A qty 2; customer cart: variant A qty 1, variant B qty 3; merge | customer cart has A qty 3, B qty 3; guest cart status=abandoned | happy path |
  1294→
  1295→---
  1296→
  1297→#### tests/Feature/Cart/CartApiTest.php
  1298→
  1299→| Test Name | Setup / Preconditions | Key Assertions | Category |

| `tests/Feature/Analytics/` | `EventIngestionTest.php`, `AggregationTest.php` |
| `tests/Feature/Webhooks/` | `WebhookDeliveryTest.php`, `WebhookSignatureTest.php` |
| `tests/Feature/Api/` | `StorefrontCartApiTest.php`, `StorefrontCheckoutApiTest.php`, `AdminProductApiTest.php`, `AdminOrderApiTest.php` |
| `tests/Browser/` | Covered in `08-PLAYWRIGHT-E2E-PLAN.md` |

---

### Unit Tests

---

#### tests/Unit/PricingEngineTest.php

**Setup:** Instantiate `PricingEngine` directly. Mock dependencies or use simple value objects.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates subtotal from line items | 2 lines: variant A 2499 cents x 2, variant B 7999 cents x 1 | subtotal = 12997 | happy path |
| calculates subtotal for a single line | 1 line: 1500 cents x 3 | subtotal = 4500 | happy path |
| returns zero subtotal for empty cart | 0 lines | subtotal = 0 | edge case |
| applies percent discount correctly | subtotal 10000, discount 10% (value_amount = 10, value_type = percent) | discount = 1000, discounted subtotal = 9000 | happy path |
| applies fixed discount correctly | subtotal 10000, discount 500 cents fixed | discount = 500, discounted subtotal = 9500 | happy path |
| caps fixed discount at subtotal so it never goes negative | subtotal 300, discount 500 cents fixed | discount = 300, discounted subtotal = 0 | edge case |
| applies free shipping discount by zeroing shipping | subtotal 5000, shipping 499, discount type free_shipping | shipping = 0 | happy path |
| calculates tax exclusive correctly | net amount 10000, rate 1900 basis points (19%) | tax = 1900, gross = 11900 | happy path |
| extracts tax from inclusive price correctly | gross amount 11900, rate 1900 basis points (19%) | tax = 1900, net = 10000 | happy path |
| returns zero tax when rate is zero | amount 10000, rate 0 | tax = 0 | edge case |
| calculates shipping flat rate | rate config type=flat, amount=499 | shipping = 499 | happy path |
| calculates full checkout totals end to end | 2 lines x 2499 = 4998, discount WELCOME10 (10%), shipping flat 499, tax 19% inclusive; pipeline: subtotal 4998 -> discount 499 -> discounted 4499 -> shipping 499 -> tax extracted from gross | total calculation matches expected value | happy path |
| handles rounding correctly with odd cent amounts | 3 lines with prices that create fractional discount allocations | sum of rounded allocations equals total discount (no off-by-one) | edge case |
| produces identical results for identical inputs | Run calculation twice with same inputs | both PricingResult objects are identical | edge case |

---

#### tests/Unit/DiscountCalculatorTest.php

**Setup:** Instantiate `DiscountService` with mock store and cart.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| validates an active discount code | discount: status=active, starts_at=yesterday, ends_at=tomorrow | validation passes, returns Discount model | happy path |
| rejects an expired discount code | discount: status=active, ends_at=yesterday | throws InvalidDiscountException with reason 'expired' | validation |
| rejects a not-yet-active discount code | discount: status=active, starts_at=tomorrow | throws InvalidDiscountException with reason 'not_yet_active' | validation |
| rejects a discount that has reached its usage limit | discount: usage_limit=10, usage_count=10 | throws InvalidDiscountException with reason 'usage_limit_reached' | validation |
| rejects an unknown discount code | look up code 'DOESNOTEXIST' | throws InvalidDiscountException with reason 'not_found' | validation |
| performs case-insensitive code lookup | discount with code 'SUMMER20'; validate code 'summer20' | validation passes | edge case |
| enforces minimum purchase amount rule | discount with rules_json containing minimum_purchase: 5000; cart subtotal: 3000 | throws InvalidDiscountException with reason 'minimum_not_met' | validation |
| passes minimum purchase when cart meets threshold | discount with rules_json minimum_purchase: 5000; cart subtotal: 5000 | validation passes | happy path |
| calculates percent discount amount | discount: 15%, subtotal: 10000 | discount amount = 1500 | happy path |
| calculates fixed discount amount | discount: 500 cents fixed, subtotal: 10000 | discount amount = 500 | happy path |
| handles free shipping discount type | discount: type=free_shipping | discount amount = 0, free_shipping flag = true | happy path |
| allocates discount proportionally across multiple lines | 2 lines: line A subtotal 7500, line B subtotal 2500 (total 10000); discount: 10% = 1000 | line A allocation = 750, line B allocation = 250 | happy path |
| distributes rounding remainder to the last qualifying line | 3 lines with subtotals that create uneven division | sum of allocations exactly equals total discount | edge case |

---

#### tests/Unit/TaxCalculatorTest.php

**Setup:** Instantiate `TaxCalculator` directly.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates manual tax exclusive | net amount: 10000, rate: 1900 (19%) | tax = 1900 | happy path |
| extracts manual tax from inclusive amount | gross amount: 11900, rate: 1900 (19%) | tax = 1900, net = 10000 | happy path |
| returns zero tax when no rate is configured | amount: 10000, rate: 0 | tax = 0 | edge case |
| handles zero amount lines | amount: 0, rate: 1900 | tax = 0 | edge case |
| calculates tax with non-standard rate | net amount: 8999, rate: 700 (7%) | tax = 629 (rounded) | happy path |
| extracts tax correctly for small amounts | gross amount: 119, rate: 1900 | tax = 19, net = 100 | edge case |
| handles high tax rates | net amount: 10000, rate: 2500 (25%) | tax = 2500 | happy path |

---

#### tests/Unit/ShippingCalculatorTest.php

**Setup:** Instantiate `ShippingCalculator` with mock zone/rate data.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| matches a zone by country code | zone countries_json: ["DE","AT","CH"]; address country: "DE" | zone matched | happy path |
| matches a zone by region code | zone regions_json: ["US-NY","US-CA"]; address: country "US", province_code "US-NY" | zone matched | happy path |
| returns empty when no zone matches the address | zone countries_json: ["DE"]; address country: "FR" | empty collection returned | edge case |
| calculates a flat rate | rate type: flat, config: amount 499 | shipping = 499 | happy path |
| calculates a weight-based rate | rate type: weight, config: ranges 0-500g=499, 501-2000g=899; cart total weight: 750g | shipping = 899 | happy path |
| calculates a price-based rate | rate type: price, config: ranges 0-5000=799, 5001-999999=399; cart subtotal: 7500 | shipping = 399 | happy path |
| returns zero shipping when no items require shipping | all cart line variants have requires_shipping = false | shipping = 0 | edge case |
| returns the correct rate when multiple zones match and the first is selected | two zones match, each with different rates | returns rates from both zones for customer selection | edge case |
| skips inactive rates | rate with is_active = 0 | rate not included in available rates | edge case |

---

#### tests/Unit/CartVersionTest.php

**Setup:** Instantiate Cart model or CartService.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| starts at version 1 | create new cart | cart_version = 1 | happy path |
| increments version on add line | add a line to cart | cart_version = 2 | happy path |
| increments version on update quantity | update line quantity | cart_version incremented by 1 | happy path |
| increments version on remove line | remove a line | cart_version incremented by 1 | happy path |
| detects version mismatch | cart at version 3, client sends expected_version 2 | CartVersionMismatchException thrown | validation |

---

#### tests/Unit/HandleGeneratorTest.php

**Setup:** Instantiate `HandleGenerator` with mock database queries.

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| generates a slug from title | title: "My Amazing Product" | handle = "my-amazing-product" | happy path |
| appends suffix on collision | title: "T-Shirt" (handle "t-shirt" already exists) | handle = "t-shirt-1" | edge case |
| increments suffix on multiple collisions | "t-shirt" and "t-shirt-1" both exist | handle = "t-shirt-2" | edge case |
| handles special characters | title: "Loewe's Fall/Winter 2026" | handle is a valid slug (e.g., "loewes-fallwinter-2026") | edge case |
| excludes current record id from collision check | product #5 with handle "t-shirt", updating product #5 | handle = "t-shirt" (no suffix needed) | edge case |
| scopes uniqueness check to store | store 1 has "t-shirt", generating for store 2 | handle = "t-shirt" (no suffix needed) | edge case |

---

### Feature Tests

All feature tests use the `RefreshDatabase` trait. Factory setup creates the necessary Store, User, and domain context.

**Shared helper setup** (in `tests/Pest.php` or trait):
- `createStoreContext()`: creates Organization, Store, StoreDomain, User with Owner role, binds `current_store` in container
- `actingAsAdmin(User $user)`: authenticates as admin and sets store in session
- `actingAsCustomer(Customer $customer)`: authenticates as customer with customer guard

---

#### tests/Feature/Tenancy/TenantResolutionTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| resolves store from hostname for storefront requests | store with domain "acme-fashion.test"; GET "/" with Host header "acme-fashion.test" | 200 response, current_store bound in container | happy path |
| returns 404 for unknown hostname | GET "/" with Host header "nonexistent.test" | 404 response | validation |
| returns 503 for suspended store on storefront | store with status "suspended"; GET "/" with Host header for suspended store | 503 response | validation |
| resolves store from session for admin requests | login as admin, set current_store_id in session; GET "/admin" | 200 response, correct store in context | happy path |
| denies admin access when user has no store_users record | login as admin, session points to store user is not associated with; GET "/admin" | 403 response | authorization |
| caches hostname lookup | first request resolves from DB; second request resolves from cache (no additional query) | cache key "store_domain:{hostname}" exists | happy path |

---

#### tests/Feature/Tenancy/StoreIsolationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| scopes product queries to the current store | store A has 3 products, store B has 5 products; set current_store to store A | Product::all() count = 3 | happy path |
| scopes order queries to the current store | store A has 2 orders, store B has 7 orders; set current_store to store A | Order::count() = 2 | happy path |
| automatically sets store_id on model creation | set current_store to store A; create a product without explicitly setting store_id | product->store_id = store A's id | happy path |
| prevents accessing another stores records via direct ID | store A product with id 1; set current_store to store B | Product::find(1) returns null | authorization |
| allows cross-store access when global scope is removed | remove StoreScope; query all products | count includes all stores | edge case |

---

#### tests/Feature/Auth/AdminAuthTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the admin login page | GET /admin/login | 200 response, sees "Login" text | happy path |
| authenticates an admin user with valid credentials | POST /admin/login with valid email/password | redirected to /admin, session has auth | happy path |
| rejects invalid credentials | POST /admin/login with wrong password | redirected back, session has errors, sees "Invalid credentials" | validation |
| does not reveal whether email or password is incorrect | POST /admin/login with non-existent email | same generic "Invalid credentials" error | validation |
| rate limits login attempts | make 6 POST requests with wrong credentials within 1 minute | 6th request returns 429 Too Many Requests | validation |
| regenerates session on successful login | record session ID before login; login successfully | session ID changed | happy path |
| logs out and invalidates session | login, then POST /admin/logout | redirected to /admin/login, session invalidated, no longer authenticated | happy path |
| redirects unauthenticated users to login | GET /admin without auth | redirected to /admin/login | authorization |
| supports remember me functionality | login with remember=true | remember_web cookie is set | happy path |
| records last_login_at on successful login | login successfully | user.last_login_at is updated to now | happy path |

---

#### tests/Feature/Auth/CustomerAuthTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the customer login page | GET /account/login on storefront domain | 200 response, sees login form | happy path |
| authenticates a customer with valid credentials | POST login with valid email/password | redirected to /account | happy path |
| rejects invalid customer credentials | POST login with wrong password | redirected back with errors | validation |
| scopes customer login to the current store | customer exists in store A but not store B; login on store B domain with store A customer credentials | login fails | authorization |
| rate limits customer login attempts | 6 attempts in 1 minute | 429 on 6th attempt | validation |
| registers a new customer | POST /account/register with name, email, password, password_confirmation | customer created in DB, auto-logged in, redirected to /account | happy path |
| rejects duplicate email registration in the same store | customer with email already exists in current store; POST /account/register with same email | validation error on email | validation |
| allows same email in different stores | customer with email in store A; register same email on store B | registration succeeds | edge case |
| logs out customer and redirects to login | login, then POST /account/logout | redirected to /account/login | happy path |
| merges guest cart into customer cart on login | guest has cart with variant A qty 2; customer has cart with variant A qty 1 and variant B qty 3; login | customer cart has variant A qty 3, variant B qty 3; guest cart abandoned | happy path |

---

#### tests/Feature/Auth/SanctumTokenTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a personal access token with abilities | acting as admin, create token with abilities ['read-products', 'write-products'] | token row in personal_access_tokens, token string returned | happy path |
| authenticates API request with valid token | GET /api/admin/v1/stores/{storeId}/products with Bearer token | 200 response | happy path |
| rejects API request with invalid token | GET /api/admin/v1/stores/{storeId}/products with fake token | 401 response | validation |
| enforces token abilities | token has only 'read-products'; POST /api/admin/v1/stores/{storeId}/products (requires write-products) | 403 response | authorization |
| revokes a token | create token, then delete it; make API request with revoked token | 401 response | happy path |

---

#### tests/Feature/Products/ProductCrudTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists products for the current store | create 5 products for current store; GET /admin/products | 200 response, sees all 5 product titles | happy path |
| creates a product with a default variant | POST via Livewire action: title, description, status=draft | product in DB, one variant with is_default=1, inventory_item created | happy path |
| generates a unique handle from the title | create product with title "Summer T-Shirt" | handle = "summer-t-shirt" | happy path |
| appends suffix when handle collides | create two products with title "T-Shirt" | handles are "t-shirt" and "t-shirt-1" | edge case |
| updates a product | create product, then update title and description | DB reflects changes | happy path |
| transitions product from draft to active | product in draft with valid variant; transition to active | status = active, published_at set | happy path |
| rejects draft to active without a priced variant | product in draft, variant price = 0; transition to active | exception thrown, status unchanged | validation |
| transitions product from active to archived | active product; transition to archived | status = archived | happy path |
| prevents active to draft when order lines exist | active product with order references; transition to draft | exception thrown | validation |
| hard deletes a draft product with no order references | draft product, no orders; delete | product removed from DB | happy path |
| prevents deletion of product with order references | product with order_lines referencing its variants; attempt delete | exception thrown | validation |
| filters products by status | 3 active, 2 draft, 1 archived; filter by status=active | 3 results | happy path |
| searches products by title | product titled "Organic Cotton Hoodie"; search "cotton" | product found in results | happy path |

---

#### tests/Feature/Products/VariantTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates variants from option matrix | product with options: Size [S, M, L], Color [Red, Blue]; rebuild matrix | 6 variants created (3x2) | happy path |
| preserves existing variants when adding an option value | existing: Size [S, M] = 2 variants; add Size L; rebuild matrix | 3 variants, original 2 unchanged (prices preserved) | happy path |
| archives orphaned variants with order references | variant referenced in order_lines; remove its option value | variant status = archived, not deleted | edge case |
| deletes orphaned variants without order references | variant not referenced in any order; remove its option value | variant deleted from DB | happy path |
| auto-creates default variant for products without options | create product with no options | 1 variant with is_default=1 | happy path |
| validates SKU uniqueness within store | create variant with SKU "TSH-001" in store A; attempt to create another variant with same SKU in store A | validation error | validation |
| allows duplicate SKU across different stores | SKU "TSH-001" in store A; create variant with "TSH-001" in store B | success | edge case |
| allows null SKUs | create two variants with null SKU | both created successfully | edge case |

---

#### tests/Feature/Products/InventoryTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates inventory item when variant is created | create product variant | inventory_item exists with quantity_on_hand=0, quantity_reserved=0 | happy path |
| checks availability correctly | on_hand=10, reserved=3 | available = 7 | happy path |
| reserves inventory | on_hand=10, reserved=0; reserve 3 | reserved=3, available=7 | happy path |
| throws InsufficientInventoryException when reserving more than available with deny policy | on_hand=5, reserved=3, policy=deny; reserve 3 | InsufficientInventoryException thrown | validation |
| allows overselling with continue policy | on_hand=2, reserved=0, policy=continue; reserve 5 | reserved=5 (no exception) | edge case |
| releases reserved inventory | on_hand=10, reserved=5; release 3 | reserved=2 | happy path |
| commits inventory on order completion | on_hand=10, reserved=3; commit 3 | on_hand=7, reserved=0 | happy path |
| restocks inventory | on_hand=5; restock 10 | on_hand=15 | happy path |

---

#### tests/Feature/Products/CollectionTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a collection with a unique handle | create collection titled "Summer Sale" | handle = "summer-sale", saved in DB | happy path |
| adds products to a collection | create collection, add 3 products | collection_products pivot has 3 rows | happy path |
| removes products from a collection | collection with 3 products, remove 1 | 2 remain | happy path |
| reorders products within a collection | 3 products with positions 0,1,2; reorder to 2,0,1 | positions updated correctly | happy path |
| transitions collection from draft to active | draft collection; transition to active | status = active | happy path |
| lists collections with product count | collection A with 5 products, collection B with 3 products; list collections | each collection shows correct product count | happy path |
| scopes collections to current store | store A has 2 collections, store B has 4; current store = A | Collection::count() = 2 | authorization |

---

#### tests/Feature/Products/MediaUploadTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| uploads an image for a product | POST Livewire file upload with a test image | product_media row created with status=processing | happy path |
| processes uploaded image and generates variants | dispatch ProcessMediaUpload job | media status = ready, file exists in storage | happy path |
| rejects non-image file types | attempt to upload a .txt file | validation error | validation |
| sets alt text on media | upload image, then update alt_text | alt_text persisted | happy path |
| reorders media positions | upload 3 images, reorder | position values updated | happy path |
| deletes media and removes file from storage | upload image, then delete | product_media row removed, file removed from disk | happy path |

---

#### tests/Feature/Cart/CartServiceTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a cart for the current store | create cart | cart in DB with store_id, currency, version=1, status=active | happy path |
| adds a line item to the cart | active product with variant priced at 2500; add to cart qty 2 | cart_line created, unit_price=2500, subtotal=5000, total=5000 | happy path |
| increments quantity when adding an existing variant | cart already has variant A qty 1; add variant A qty 2 | single line with qty 3, subtotal=7500 | happy path |
| rejects add when product is not active | product status = draft; attempt to add variant to cart | exception thrown | validation |
| rejects add when inventory is insufficient and policy is deny | variant on_hand=2, policy=deny; attempt to add qty 5 | InsufficientInventoryException | validation |
| allows add when inventory is insufficient but policy is continue | variant on_hand=2, policy=continue; add qty 5 | line created with qty 5 | edge case |
| updates line quantity | cart with variant A qty 2; update to qty 5 | line quantity=5, amounts recalculated | happy path |
| removes a line when quantity set to zero | update line quantity to 0 | line removed from cart | edge case |
| removes a specific line item | cart with 2 lines, remove first | 1 line remains | happy path |
| increments cart version on every mutation | create cart (v1), add line (v2), update qty (v3), remove line (v4) | cart_version = 4 | happy path |
| returns cart via session for guest users | create cart, store ID in session; get cart for session | same cart returned | happy path |
| merges guest cart into customer cart on login | guest cart: variant A qty 2; customer cart: variant A qty 1, variant B qty 3; merge | customer cart has A qty 3, B qty 3; guest cart status=abandoned | happy path |

---

#### tests/Feature/Cart/CartApiTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |


I have a thorough understanding of the spec. Let me create the team and get started.


TeamCreate

Building the complete e-commerce shop system per specs


Now let me create the initial progress tracking file and tasks for Phase 1 (foundation - everything depends on this).


Write

# Implementation Progress

## Status: IN PROGRESS

### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations
- [ ] Step 1.3: Core Models (Organization, Store, StoreDomain, StoreUser, StoreSettings)
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication (Admin + Customer)
- [ ] Step 1.8: Authorization (Policies)
- [ ] Phase 1 Tests

### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
- [ ] Step 2.1: Migrations
- [ ] Step 2.2: Models with Relationships
- [ ] Step 2.3: Product Service
- [ ] Step 2.4: Inventory Service
- [ ] Step 2.5: Media Upload
- [ ] Phase 2 Tests

### Phase 3: Themes, Pages, Navigation, Storefront Layout
- [ ] Step 3.1: Migrations
- [ ] Step 3.2: Models
- [ ] Step 3.3: Storefront Blade Layout
- [ ] Step 3.4: Storefront Livewire Components
- [ ] Step 3.5: Navigation Service
- [ ] Phase 3 Tests

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
- [ ] Step 4.1: Migrations
- [ ] Step 4.2: Models
- [ ] Step 4.3: Cart Service
- [ ] Step 4.4: Discount Service
- [ ] Step 4.5: Shipping Calculator
- [ ] Step 4.6: Tax Calculator
- [ ] Step 4.7: Pricing Engine
- [ ] Step 4.8: Checkout State Machine
- [ ] Step 4.9: Storefront Cart/Checkout UI
- [ ] Phase 4 Tests

### Phase 5: Payments, Orders, Fulfillment
- [ ] Step 5.1: Migrations
- [ ] Step 5.2: Models
- [ ] Step 5.3: Payment Service (Mock PSP)
- [ ] Step 5.4: Order Service
- [ ] Step 5.5: Refund Service
- [ ] Step 5.6: Fulfillment Service
- [ ] Phase 5 Tests

### Phase 6: Customer Accounts
- [ ] Step 6.1: Customer Auth
- [ ] Step 6.2: Customer Account Pages
- [ ] Phase 6 Tests

### Phase 7: Admin Panel
- [ ] Step 7.1: Admin Layout
- [ ] Step 7.2: Dashboard
- [ ] Step 7.3: Product Management
- [ ] Step 7.4: Order Management
- [ ] Step 7.5: Other Admin Sections
- [ ] Phase 7 Tests

### Phase 8: Search
- [ ] Step 8.1: Migrations
- [ ] Step 8.2: Search Service
- [ ] Step 8.3: Search UI
- [ ] Phase 8 Tests

### Phase 9: Analytics
- [ ] Step 9.1: Migrations
- [ ] Step 9.2: Analytics Service
- [ ] Phase 9 Tests

### Phase 10: Apps and Webhooks
- [ ] Step 10.1: Migrations
- [ ] Step 10.2: Webhook Service
- [ ] Phase 10 Tests

### Phase 11: Polish
- [ ] Accessibility, responsive, dark mode, error pages, logging, seed data

### Phase 12: Full Test Suite
- [ ] All unit/feature tests pass
- [ ] All browser/E2E tests pass
- [ ] Code style passes
- [ ] Fresh migration + seed succeeds
- [ ] Playwright review meeting


File created successfully at: /Users/fabianwesner/Herd/shop/specs/progress.md

# Implementation Progress

## Status: IN PROGRESS

### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations
- [ ] Step 1.3: Core Models (Organization, Store, StoreDomain, StoreUser, StoreSettings)
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication (Admin + Customer)
- [ ] Step 1.8: Authorization (Policies)
- [ ] Phase 1 Tests

### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
- [ ] Step 2.1: Migrations
- [ ] Step 2.2: Models with Relationships
- [ ] Step 2.3: Product Service
- [ ] Step 2.4: Inventory Service
- [ ] Step 2.5: Media Upload
- [ ] Phase 2 Tests

### Phase 3: Themes, Pages, Navigation, Storefront Layout
- [ ] Step 3.1: Migrations
- [ ] Step 3.2: Models
- [ ] Step 3.3: Storefront Blade Layout
- [ ] Step 3.4: Storefront Livewire Components
- [ ] Step 3.5: Navigation Service
- [ ] Phase 3 Tests

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
- [ ] Step 4.1: Migrations
- [ ] Step 4.2: Models
- [ ] Step 4.3: Cart Service
- [ ] Step 4.4: Discount Service
- [ ] Step 4.5: Shipping Calculator
- [ ] Step 4.6: Tax Calculator
- [ ] Step 4.7: Pricing Engine
- [ ] Step 4.8: Checkout State Machine
- [ ] Step 4.9: Storefront Cart/Checkout UI
- [ ] Phase 4 Tests

### Phase 5: Payments, Orders, Fulfillment
- [ ] Step 5.1: Migrations
- [ ] Step 5.2: Models
- [ ] Step 5.3: Payment Service (Mock PSP)
- [ ] Step 5.4: Order Service
- [ ] Step 5.5: Refund Service
- [ ] Step 5.6: Fulfillment Service
- [ ] Phase 5 Tests

### Phase 6: Customer Accounts
- [ ] Step 6.1: Customer Auth
- [ ] Step 6.2: Customer Account Pages
- [ ] Phase 6 Tests

### Phase 7: Admin Panel
- [ ] Step 7.1: Admin Layout
- [ ] Step 7.2: Dashboard
- [ ] Step 7.3: Product Management
- [ ] Step 7.4: Order Management
- [ ] Step 7.5: Other Admin Sections
- [ ] Phase 7 Tests

### Phase 8: Search
- [ ] Step 8.1: Migrations
- [ ] Step 8.2: Search Service
- [ ] Step 8.3: Search UI
- [ ] Phase 8 Tests

### Phase 9: Analytics
- [ ] Step 9.1: Migrations
- [ ] Step 9.2: Analytics Service
- [ ] Phase 9 Tests

### Phase 10: Apps and Webhooks
- [ ] Step 10.1: Migrations
- [ ] Step 10.2: Webhook Service
- [ ] Phase 10 Tests

### Phase 11: Polish
- [ ] Accessibility, responsive, dark mode, error pages, logging, seed data

### Phase 12: Full Test Suite
- [ ] All unit/feature tests pass
- [ ] All browser/E2E tests pass
- [ ] Code style passes
- [ ] Fresh migration + seed succeeds
- [ ] Playwright review meeting


Now let me create tasks and spawn teammates. Phase 1 is critical and sequential, so I'll have one teammate start on it. Once Phase 1 is done, we'll parallelize Phase 2 and Phase 3.


TaskCreate

Phase 1: Foundation - Migrations, Models, Middleware, Auth

Implement the entire Phase 1 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. This includes:

1. **Step 1.1 Environment/Config**: Configure auth.php with customer guard/provider, database.php SQLite optimizations (WAL, FK, busy_timeout), logging JSON channel
2. **Step 1.2 Core Migrations**: organizations, stores, store_domains, modify users table (add status/last_login_at), store_users pivot, store_settings
3. **Step 1.3 Core Models**: Organization, Store, StoreDomain, StoreUser (pivot), StoreSettings with all relationships, factories, casts
4. **Step 1.4 Enums**: StoreStatus, StoreUserRole, StoreDomainType in app/Enums/
5. **Step 1.5 ResolveStore Middleware**: Storefront hostname resolution with caching, admin session-based, 404 for unknown, 503 for suspended
6. **Step 1.6 BelongsToStore Trait + StoreScope**: Global scope for tenant isolation, auto-set store_id on creating
7. **Step 1.7 Authentication**: Admin auth (session-based, rate limited), Customer auth (custom guard, CustomerUserProvider scoped by store_id, email unique per store)
8. **Step 1.8 Authorization**: All policies (Product, Order, Collection, Discount, Customer, Store, Page, Theme, Fulfillment, Refund) with permission matrix, roleForStore helper on User model

Also set up shared test helpers in tests/Pest.php: createStoreContext(), actingAsAdmin(), actingAsCustomer()

Write all Phase 1 tests: TenantResolutionTest, StoreIsolationTest, AdminAuthTest, CustomerAuthTest

Reference specs/01-DATABASE-SCHEMA.md for exact column definitions.
Reference specs/06-AUTH-AND-SECURITY.md for auth details.
Reference specs/09-IMPLEMENTATION-ROADMAP.md Phase 1 for implementation details.

IMPORTANT: Run tests after implementation to ensure they pass. Use `php artisan test --filter=` to run specific tests.


Task #1 created successfully: Phase 1: Foundation - Migrations, Models, Middleware, Auth


Phase 2: Catalog - Products, Variants, Inventory, Collections, Media

Implement Phase 2 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 1.

1. **Step 2.1 Migrations**: products, product_options, product_option_values, product_variants, variant_option_values, inventory_items, collections, collection_products, product_media
2. **Step 2.2 Models**: Product, ProductOption, ProductOptionValue, ProductVariant, InventoryItem, Collection, ProductMedia with all relationships and factories
3. **Step 2.3 ProductService**: create, update, transitionStatus, delete. VariantMatrixService: rebuildMatrix. HandleGenerator: generate unique slugs
4. **Step 2.4 InventoryService**: checkAvailability, reserve, release, commit, restock with transactions and InsufficientInventoryException
5. **Step 2.5 Media Upload**: ProcessMediaUpload job, Livewire file uploads

Enums: ProductStatus, VariantStatus, CollectionStatus, MediaType, MediaStatus, InventoryPolicy

Write all Phase 2 tests: ProductCrudTest, VariantTest, InventoryTest, CollectionTest, MediaUploadTest, HandleGeneratorTest

Reference specs/01-DATABASE-SCHEMA.md for column definitions.
Reference specs/05-BUSINESS-LOGIC.md for business rules.
Run tests after implementation.


Task #2 created successfully: Phase 2: Catalog - Products, Variants, Inventory, Collections, Media


Phase 3: Themes, Pages, Navigation, Storefront Layout

Implement Phase 3 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 1.

1. **Step 3.1 Migrations**: themes, theme_files, theme_settings, pages, navigation_menus, navigation_items
2. **Step 3.2 Models**: Theme, ThemeFile, ThemeSettings, Page, NavigationMenu, NavigationItem with relationships and factories
3. **Step 3.3 Storefront Blade Layout**: resources/views/storefront/ directory with app.blade.php layout (header, nav, announcement bar, footer, cart drawer), page templates, components (product-card, price, badge, quantity-selector, etc.). Dark mode support.
4. **Step 3.4 Storefront Livewire Components**: Home, Collections\Index, Collections\Show, Products\Show, Cart\Show, CartDrawer, Search\Index, Search\Modal, Pages\Show
5. **Step 3.5 NavigationService**: buildTree, resolveUrl with caching

Enums: ThemeStatus, PageStatus, NavigationItemType

Create ThemeSettings singleton service in AppServiceProvider.

Reference specs/04-STOREFRONT-UI.md for storefront design.
Reference specs/01-DATABASE-SCHEMA.md for column definitions.

Write tests for theme/page/navigation functionality.
Run tests after implementation.


Task #3 created successfully: Phase 3: Themes, Pages, Navigation, Storefront Layout


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

Implement Phase 4 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 2 and Phase 3.

1. **Step 4.1 Migrations**: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts
2. **Step 4.2 Models**: Cart, CartLine, Checkout, ShippingZone, ShippingRate, TaxSettings, Discount with relationships
3. **Step 4.3 CartService**: create, addLine, updateLineQuantity, removeLine, getOrCreateForSession, mergeOnLogin. Cart versioning.
4. **Step 4.4 DiscountService**: validate (code lookup, dates, usage limits, min purchase), calculate (percent/fixed/free_shipping), proportional allocation
5. **Step 4.5 ShippingCalculator**: getAvailableRates (zone matching), calculate (flat/weight/price)
6. **Step 4.6 TaxCalculator**: calculate, extractInclusive, addExclusive. All integer math, rates in basis points.
7. **Step 4.7 PricingEngine**: Full pipeline - line subtotals -> cart subtotal -> discount -> shipping -> tax -> total. PricingResult and TaxLine value objects.
8. **Step 4.8 CheckoutService**: State machine (started->addressed->shipping_selected->payment_selected->completed). ExpireAbandonedCheckouts job, CleanupAbandonedCarts job.
9. **Step 4.9 Cart/Checkout UI**: CartDrawer, Cart\Show, Checkout\Show (multi-step), Checkout\Confirmation

Enums: CartStatus, CheckoutStatus, DiscountType, DiscountValueType, DiscountStatus, ShippingRateType, TaxMode

Write all tests: CartServiceTest, CartApiTest, CheckoutFlowTest, CheckoutStateTest, PricingIntegrationTest, DiscountTest, ShippingTest, TaxTest, plus unit tests (PricingEngineTest, DiscountCalculatorTest, TaxCalculatorTest, ShippingCalculatorTest, CartVersionTest)

Reference specs/05-BUSINESS-LOGIC.md for business rules.
Run tests after implementation.


Task #4 created successfully: Phase 4: Cart, Checkout, Discounts, Shipping, Taxes


Phase 5: Payments, Orders, Fulfillment

Implement Phase 5 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 4.

1. **Step 5.1 Migrations**: customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines
2. **Step 5.2 Models**: Customer, CustomerAddress, Order, OrderLine, Payment, Refund, Fulfillment, FulfillmentLine with relationships
3. **Step 5.3 Payment Service**: PaymentProvider interface, MockPaymentProvider (magic card numbers: 4242...=success, 4000...0002=decline, 4000...9995=insufficient). Credit card/PayPal instant capture, bank transfer deferred. Admin payment confirmation for bank transfers. CancelUnpaidBankTransferOrders job.
4. **Step 5.4 OrderService**: createFromCheckout (atomic), generateOrderNumber (#1001 sequential), cancel
5. **Step 5.5 RefundService**: create with partial/full support, restock flag
6. **Step 5.6 FulfillmentService**: create (with payment guard), markAsShipped, markAsDelivered. FulfillmentGuardException.

Enums: OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus

Events: OrderCreated, OrderPaid, OrderFulfilled, OrderCancelled, OrderRefunded

Write tests: OrderCreationTest, RefundTest, FulfillmentTest, MockPaymentProviderTest, PaymentServiceTest, BankTransferConfirmationTest

Run tests after implementation.


Task #5 created successfully: Phase 5: Payments, Orders, Fulfillment

Phase 5: Payments, Orders, Fulfillment


Phase 6: Customer Accounts

Implement Phase 6 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 5.

1. **Step 6.1**: Customer auth with custom guard (already configured in Phase 1), CustomerUserProvider scoped by store_id
2. **Step 6.2 Customer Account Pages** (Livewire components):
   - Storefront\Account\Auth\Login (GET /account/login)
   - Storefront\Account\Auth\Register (GET /account/register)
   - 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
   - POST /account/logout - invalidate session, redirect to login

Write tests: CustomerAccountTest, AddressManagementTest
Run tests after implementation.


Task #6 created successfully: Phase 6: Customer Accounts

Phase 6: Customer Accounts


Phase 7: Admin Panel

Implement Phase 7 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 5.

1. **Step 7.1 Admin Layout**: Sidebar (256px, nav groups, Flux brand/icon/separator), TopBar (store selector, user profile), Breadcrumbs
2. **Step 7.2 Dashboard**: KPI tiles (revenue, orders, AOV, conversion), orders chart (30 days), recent orders, date range filtering
3. **Step 7.3 Product Management**: Products\Index (search, filter, bulk actions, pagination), Products\Form (shared create/edit, title, description, status, variants builder, media upload)
4. **Step 7.4 Order Management**: Orders\Index (status filters, search, date range), Orders\Show (timeline, line items, payment, fulfillment modal, refund modal)
5. **Step 7.5 Other Admin Sections**:
   - Collections\Index + Form (product picker, reorder)
   - Customers\Index + Show (info, orders, addresses)
   - Discounts\Index + Form (code, type, value, dates, rules)
   - Settings\Index (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications)
   - Settings\Shipping (zones CRUD, rates CRUD)
   - Settings\Taxes (tax mode, rate config)
   - Themes\Index + Editor
   - Pages\Index + Form (rich text)
   - Navigation\Index (menu builder)
   - Analytics\Index (charts, date range)

Reference specs/03-ADMIN-UI.md for detailed UI specs.
Use Flux UI Free components.

Write tests: Admin DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest
Run tests after implementation.


Task #7 created successfully: Phase 7: Admin Panel

Phase 7: Admin Panel


Phase 8: Search (FTS5)

Implement Phase 8 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 2.

1. **Step 8.1 Migrations**: search_settings, search_queries, FTS5 virtual table (products_fts)
2. **Step 8.2 SearchService**: search (FTS5 with store scoping), autocomplete (prefix matching), syncProduct, removeProduct. ProductObserver for sync.
3. **Step 8.3 Search UI**: Search\Modal (autocomplete), Search\Index (full results with filters, sort, pagination)

Write tests: SearchTest, AutocompleteTest
Run tests after implementation.


Task #8 created successfully: Phase 8: Search (FTS5)

Phase 8: Search (FTS5)


Phase 9: Analytics

Implement Phase 9 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 5.

1. **Step 9.1 Migrations**: analytics_events, analytics_daily
2. **Step 9.2 AnalyticsService**: track events, getDailyMetrics. AggregateAnalytics job (daily via console.php).
Event types: page_view, product_view, add_to_cart, remove_from_cart, checkout_started, checkout_completed, search.

Write tests: EventIngestionTest, AggregationTest
Run tests after implementation.


Task #9 created successfully: Phase 9: Analytics

Phase 9: Analytics


Phase 10: Apps and Webhooks

Implement Phase 10 as specified in specs/09-IMPLEMENTATION-ROADMAP.md. Blocked by Phase 5.

1. **Step 10.1 Migrations**: apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries
2. **Step 10.2 WebhookService**: dispatch, sign (HMAC-SHA256), verify. DeliverWebhook job with retry + circuit breaker.

Write tests: WebhookDeliveryTest, WebhookSignatureTest
Run tests after implementation.


Task #10 created successfully: Phase 10: Apps and Webhooks

Phase 10: Apps and Webhooks


Phase 11: Polish

Implement Phase 11. Blocked by Phases 6, 7, 8, 9, 10.

1. Accessibility: skip links, ARIA labels, focus management
2. Responsive: all pages at sm/md/lg/xl breakpoints
3. Dark mode: dark: prefix on all views
4. Error pages: styled 404 and 503
5. Structured logging: JSON channel
6. Comprehensive seed data for demo stores (per specs/07-SEEDERS-AND-TEST-DATA.md)


Task #11 created successfully: Phase 11: Polish

Phase 11: Polish


Phase 12: Full Test Suite + Browser Tests + Review

Implement Phase 12. Blocked by Phase 11.

1. Run full unit/feature test suite - all must pass
2. Run all browser/E2E tests per specs/08-PLAYWRIGHT-E2E-PLAN.md (101 tests across 10 suites)
3. Run code style (pint --dirty)
4. Fresh migration + seed verification
5. Playwright smoke: storefront navigation, add to cart, checkout
6. Playwright smoke: admin login, product management, order management
7. Fix any bugs found
8. Full review meeting showcasing all features


Task #12 created successfully: Phase 12: Full Test Suite + Browser Tests + Review


Now let me set up task dependencies and spawn the teammates.


Updated task #2 blockedBy


Updated task #3 blockedBy


Updated task #4 blockedBy


Updated task #5 blockedBy


Updated task #6 blockedBy


Updated task #7 blockedBy


Updated task #8 blockedBy


Updated task #9 blockedBy


Updated task #10 blockedBy


Updated task #11 blockedBy


Updated task #12 blockedBy


Good, dependencies are set. Now let me spawn 3 teammates. The first one starts on Phase 1 (the critical foundation). The other two will wait until Phase 1 is done, then work on Phase 2 and Phase 3 in parallel.


Updated task #1 owner, status


Agent

Phase 1: Foundation build

Prompt:
You are "backend-1", a backend engineer on team "shop-build". Your task is to implement Phase 1: Foundation for the e-commerce platform.

Read your assigned task #1 via TaskGet for full details, then read the relevant spec files. The team config is at ~/.claude/teams/drifting-exploring-hartmanis/config.json.

## CRITICAL INSTRUCTIONS

1. Read specs/09-IMPLEMENTATION-ROADMAP.md (Phase 1 section), specs/01-DATABASE-SCHEMA.md, specs/06-AUTH-AND-SECURITY.md for complete requirements
2. Follow CLAUDE.md guidelines strictly (Laravel conventions, Pest testing, Flux UI, etc.)
3. Use `php artisan make:` commands where possible
4. Run `vendor/bin/pint --dirty` before finishing
5. Run tests with `php artisan test` to verify everything passes
6. Mark your task as completed when done via TaskUpdate
7. After completing, check TaskList for next available task

## IMPLEMENTATION ORDER (STRICT)

### Step 1.1: Environment and Config
- Update config/auth.php: Add 'customer' guard with session driver + 'customers' provider. Add 'customers' provider pointing to Customer model. Add 'customers' password broker.
- Update config/database.php: Add SQLite WAL mode, foreign_keys, busy_timeout=5000, synchronous=normal
- Register rate limiters in AppServiceProvider::boot() for login (5/min per IP)

### Step 1.2: Core Migrations
Create migrations in this EXACT order (the existing users table migration already exists, so modify it):
1. create_organizations_table: id, name, slug (unique), billing_email, status (text, default 'active'), created_at, updated_at
2. create_stores_table: id, organization_id (FK), name, slug (unique), status (text CHECK active/suspended, default 'active'), currency (text, default 'USD'), locale (text, default 'en'), timezone (text, default 'UTC'), created_at, updated_at
3. create_store_domains_table: id, store_id (FK), domain (unique), type (text CHECK storefront/admin/api, default 'storefront'), is_primary (boolean, default 0), created_at, updated_at
4. Modify users table: add store_id (nullable), status (text, default 'active'), last_login_at (timestamp nullable). Keep existing columns.
5. create_store_users_table: id, store_id (FK), user_id (FK), role (text CHECK owner/admin/staff/support), created_at, updated_at. UNIQUE constraint on (store_id, user_id)
6. create_store_settings_table: id, store_id (FK unique), general_json (text nullable), checkout_json (text nullable), notifications_json (text nullable), created_at, updated_at

### Step 1.3: Core Models
Create Organization, Store, StoreDomain, StoreUser (as Pivot), StoreSettings models with:
- All relationships per spec
- Proper $fillable arrays
- casts() method for JSON/enum columns
- Factories with meaningful defaults
- Each model must use HasFactory

### Step 1.4: Enums
Create in app/Enums/:
- StoreStatus: Active, Suspended (backed by string: 'active', 'suspended')
- StoreUserRole: Owner, Admin, Staff, Support (backed by string)
- StoreDomainType: Storefront, Admin, Api (backed by string)

### Step 1.5: ResolveStore Middleware
Create App\Http\Middleware\ResolveStore:
- For storefront routes: extract hostname, lookup store_domains, cache 5 min, bind Store as 'current_store' singleton
- For admin routes: read current_store_id from session, verify store_users record exists
- 404 for unknown hostname
- 503 for suspended store on storefront
- Register in bootstrap/app.php as middleware aliases

### Step 1.6: BelongsToStore Trait + StoreScope
- App\Models\Concerns\BelongsToStore trait: applies StoreScope, auto-sets store_id on 'creating' event
- App\Models\Scopes\StoreScope: adds where('store_id', app('current_store')->id) but only when current_store is bound

### Step 1.7: Authentication
Admin auth:
- Create Livewire component Admin\Auth\Login at app/Livewire/Admin/Auth/Login.php
- Create Livewire component Admin\Auth\Logout
- Session-based auth via Auth::guard('web')
- Rate limiting 5/min per IP
- Create admin routes in routes/web.php under /admin prefix

Customer auth:
- Create Customer model (will be expanded in Phase 5, but needs basic version for auth)
- Create App\Auth\CustomerUserProvider that scopes by store_id
- Register in AuthServiceProvider or AppServiceProvider
- Create Livewire components Storefront\Account\Auth\Login and Register
- Customer email unique per store, not globally
- Rate limiting 5/min per IP
- Create storefront account routes

### Step 1.8: Authorization
Create policies in app/Policies/:
- ProductPolicy, OrderPolicy, CollectionPolicy, DiscountPolicy, CustomerPolicy, StorePolicy, PagePolicy, ThemePolicy, FulfillmentPolicy, RefundPolicy
- Each checks user role via store_users pivot
- Add roleForStore(Store $store): ?StoreUserRole helper on User model
- Permission matrix as in the spec

### Test Helpers (tests/Pest.php)
Add helper functions:
- createStoreContext(): creates Organization, Store, StoreDomain, User with Owner role, binds current_store
- actingAsAdmin(User $user): authenticates and sets store in session
- actingAsCustomer(Customer $customer): authenticates with customer guard

### Tests to Write
- tests/Feature/Tenancy/TenantResolutionTest.php (6 tests)
- tests/Feature/Tenancy/StoreIsolationTest.php (5 tests)
- tests/Feature/Auth/AdminAuthTest.php (10 tests)
- tests/Feature/Auth/CustomerAuthTest.php (10 tests)

ALL tests must pass before marking the task complete.

## IMPORTANT NOTES
- The existing User model has Fortify 2FA integration - preserve that
- The existing auth views/routes from Fortify should remain functional
- Use `php artisan make:model ModelName -mf` to create model with migration and factory
- Monetary amounts are INTEGER in cents
- All enums stored as TEXT with CHECK constraints in SQLite
- Run `vendor/bin/pint --dirty` to fix code style
- Commit your changes with a meaningful message when done