content:
   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` |
  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→---

content:
#### 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` |
| `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 |

---


userType: external

gitBranch: claude-subagents

slug: wise-shimmying-pretzel

type: message

role: assistant

type: thinking

thinking:
Now I have a good understanding of what needs to be done for Phase 6, 7, 9, and 10. Let me launch these phases in parallel as sub-agents. Phase 6 (Customer Accounts) and Phase 7 (Admin Panel) are both independent of each other (both depend on Phase 5 which is done). Phase 9 (Analytics) and Phase 10 (Apps/Webhooks) also depend on Phase 5 which is done.

Let me read the storefront spec and admin spec for more details needed by the agents, and then also the existing routes to understand what's already there.

Let me launch the agents now:

I'll prepare comprehensive prompts for each of the three parallel workstreams so they have all the context they need to proceed independently.

service_tier: standard

type: assistant


type: text

text: Now let me read the existing route files and customer-related code before launching the next phases.


type: progress

hookEvent: PostToolUse

command: callback


type: user

role: user

content:
     1→<?php
     2→
     3→use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
     4→use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
     5→use App\Livewire\Storefront\Account\Dashboard as AccountDashboard;
     6→use App\Livewire\Storefront\Cart\Show as CartShow;
     7→use App\Livewire\Storefront\Checkout\Confirmation as CheckoutConfirmation;
     8→use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
     9→use App\Livewire\Storefront\Collections\Index as CollectionsIndex;
    10→use App\Livewire\Storefront\Collections\Show as CollectionShow;
    11→use App\Livewire\Storefront\Home;
    12→use App\Livewire\Storefront\Pages\Show as PageShow;
    13→use App\Livewire\Storefront\Products\Show as ProductShow;
    14→use App\Livewire\Storefront\Search\Index as SearchIndex;
    15→use Illuminate\Support\Facades\Route;
    16→
    17→/*
    18→|--------------------------------------------------------------------------
    19→| Storefront Routes
    20→|--------------------------------------------------------------------------
    21→*/
    22→
    23→// Public storefront routes
    24→Route::middleware(['store.resolve'])->group(function () {
    25→    Route::get('/', Home::class)->name('storefront.home');
    26→    Route::get('/collections', CollectionsIndex::class)->name('storefront.collections.index');
    27→    Route::get('/collections/{handle}', CollectionShow::class)->name('storefront.collections.show');
    28→    Route::get('/products/{handle}', ProductShow::class)->name('storefront.products.show');
    29→    Route::get('/cart', CartShow::class)->name('storefront.cart');
    30→    Route::get('/checkout', CheckoutShow::class)->name('storefront.checkout');
    31→    Route::get('/checkout/confirmation/{checkout}', CheckoutConfirmation::class)->name('storefront.checkout.confirmation');
    32→    Route::get('/search', SearchIndex::class)->name('storefront.search');
    33→    Route::get('/pages/{handle}', PageShow::class)->name('storefront.pages.show');
    34→
    35→    // Customer auth routes
    36→    Route::get('/account/login', CustomerLogin::class)->name('customer.login');
    37→    Route::get('/account/register', CustomerRegister::class)->name('customer.register');
    38→
    39→    // Customer protected routes
    40→    Route::middleware(['auth:customer'])->prefix('account')->group(function () {
    41→        Route::get('/', AccountDashboard::class)->name('customer.dashboard');
    42→    });
    43→});
    44→

content:
<?php

use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
use App\Livewire\Storefront\Account\Dashboard as AccountDashboard;
use App\Livewire\Storefront\Cart\Show as CartShow;
use App\Livewire\Storefront\Checkout\Confirmation as CheckoutConfirmation;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Livewire\Storefront\Collections\Index as CollectionsIndex;
use App\Livewire\Storefront\Collections\Show as CollectionShow;
use App\Livewire\Storefront\Home;
use App\Livewire\Storefront\Pages\Show as PageShow;
use App\Livewire\Storefront\Products\Show as ProductShow;
use App\Livewire\Storefront\Search\Index as SearchIndex;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Storefront Routes
|--------------------------------------------------------------------------
*/

// Public storefront routes
Route::middleware(['store.resolve'])->group(function () {
    Route::get('/', Home::class)->name('storefront.home');
    Route::get('/collections', CollectionsIndex::class)->name('storefront.collections.index');
    Route::get('/collections/{handle}', CollectionShow::class)->name('storefront.collections.show');
    Route::get('/products/{handle}', ProductShow::class)->name('storefront.products.show');
    Route::get('/cart', CartShow::class)->name('storefront.cart');
    Route::get('/checkout', CheckoutShow::class)->name('storefront.checkout');
    Route::get('/checkout/confirmation/{checkout}', CheckoutConfirmation::class)->name('storefront.checkout.confirmation');
    Route::get('/search', SearchIndex::class)->name('storefront.search');
    Route::get('/pages/{handle}', PageShow::class)->name('storefront.pages.show');

    // Customer auth routes
    Route::get('/account/login', CustomerLogin::class)->name('customer.login');
    Route::get('/account/register', CustomerRegister::class)->name('customer.register');

    // Customer protected routes
    Route::middleware(['auth:customer'])->prefix('account')->group(function () {
        Route::get('/', AccountDashboard::class)->name('customer.dashboard');
    });
});


content:
     1→<?php
     2→
     3→use App\Livewire\Admin\Auth\Login;
     4→use App\Livewire\Admin\Dashboard;
     5→use Illuminate\Support\Facades\Route;
     6→
     7→/*
     8→|--------------------------------------------------------------------------
     9→| Admin Routes
    10→|--------------------------------------------------------------------------
    11→*/
    12→
    13→// Admin auth routes (no auth required)
    14→Route::get('/admin/login', Login::class)->name('admin.login');
    15→
    16→// Admin protected routes
    17→Route::prefix('admin')->middleware(['auth', 'store.resolve'])->group(function () {
    18→    Route::get('/', Dashboard::class)->name('admin.dashboard');
    19→});
    20→

content:
<?php

use App\Livewire\Admin\Auth\Login;
use App\Livewire\Admin\Dashboard;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Admin Routes
|--------------------------------------------------------------------------
*/

// Admin auth routes (no auth required)
Route::get('/admin/login', Login::class)->name('admin.login');

// Admin protected routes
Route::prefix('admin')->middleware(['auth', 'store.resolve'])->group(function () {
    Route::get('/', Dashboard::class)->name('admin.dashboard');
});


content:
     1→# 04 - Storefront UI/UX Specification
     2→
     3→> E-commerce platform - Customer-facing storefront UI/UX requirements
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
    12→| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
    13→| Templating | Blade templates with reusable storefront-prefixed components |
    14→| Accessibility | WCAG 2.2 AA compliance throughout |
    15→| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
    16→| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
    17→| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |
    18→
    19→---
    20→
    21→## Currency Formatting
    22→
    23→All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:
    24→
    25→| Setting | Value |
    26→|---------|-------|
    27→| Decimal separator | `.` (period) |
    28→| Thousands separator | `,` (comma) |
    29→| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
    30→| Decimal places | Always 2 |
    31→| Free amounts | Display as `0.00 EUR` |
    32→| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |
    33→
    34→**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.
    35→
    36→**Examples:**
    37→- 2499 cents -> `24.99 EUR`
    38→- 0 cents -> `0.00 EUR`
    39→- 149900 cents -> `1,499.00 EUR`
    40→
    41→---
    42→
    43→## 1. Theme System Architecture
    44→
    45→### 1.1 View Organization
    46→
    47→The storefront views are organized into the following categories:
    48→
    49→| Category | Purpose | Views |
    50→|---|---|---|
    51→| Layouts | Master page shell | app (master layout) |
    52→| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
    53→| Checkout | Purchase flow | checkout index, order confirmation |
    54→| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
    55→| Errors | Error states | 404, 503 |
    56→| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |
    57→
    58→### 1.2 Layout Hierarchy
    59→
    60→All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.
    61→
    62→### 1.3 Theme Settings
    63→
    64→Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:
    65→
    66→- Announcement bar visibility, text, link, and background color
    67→- Sticky header toggle
    68→- Home page section order and visibility
    69→- Color palette overrides (primary, secondary, accent)
    70→- Typography scale
    71→- Footer content and social links
    72→- Dark mode behavior (system preference, toggle, or forced)
    73→
    74→### 1.4 Section-Based Architecture
    75→
    76→Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.
    77→
    78→| Section | Description | Used On |
    79→|---|---|---|
    80→| Hero | Full-width banner with heading, subheading, call-to-action | Home |
    81→| Featured Collections | Grid of collection cards | Home |
    82→| Featured Products | Grid of product cards (lazy-loaded) | Home |
    83→| Newsletter | Email signup form | Home, Footer |
    84→| Rich Text | Customizable content block | Home, Pages |
    85→
    86→### 1.5 Blade Components
    87→
    88→All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.
    89→
    90→---
    91→
    92→## 2. Base Layout
    93→
    94→### 2.1 Document Head
    95→
    96→The document head includes:
    97→
    98→| Element | Description |
    99→|---|---|
   100→| Character encoding | UTF-8 |
   101→| Viewport | Width = device width, initial scale 1 |
   102→| Meta description | Page-specific, yielded from child templates |
   103→| Title | Page-specific title appended with the app name |
   104→| Stylesheets | Compiled CSS via Vite |
   105→| Scripts | Compiled JS via Vite |
   106→| Livewire styles | Injected by Livewire |
   107→| Language attribute | Set on the HTML element from application locale |
   108→| Scroll behavior | Smooth scrolling enabled globally |
   109→
   110→### 2.2 Skip Link
   111→
   112→A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.
   113→
   114→**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
   115→
   116→### 2.3 Announcement Bar
   117→
   118→Optional, controlled by theme settings. Renders a full-width bar above the header.
   119→
   120→| Attribute | Description |
   121→|---|---|
   122→| Position | Full-width, above the header |
   123→| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
   124→| Text | Centered, small size, configurable content |
   125→| Link | Optional, underlined, opens in same tab |
   126→| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
   127→| Height | Auto height with vertical padding |
   128→
   129→### 2.4 Header
   130→
   131→The header serves as the primary navigation banner.
   132→
   133→**Desktop Layout (large breakpoint and above):**
   134→
   135→```
   136→+--------------------------------------------------------------+
   137→| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
   138→+--------------------------------------------------------------+
   139→```
   140→
   141→| Element | Position | Description |
   142→|---|---|---|
   143→| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
   144→| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
   145→| Search Icon | Right group | Button that opens the search modal |
   146→| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
   147→| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |
   148→
   149→**Mobile Layout (below large breakpoint):**
   150→

content:
# 04 - Storefront UI/UX Specification

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

---

## Conventions

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

---

## Currency Formatting

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

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

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

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

---

## 1. Theme System Architecture

### 1.1 View Organization

The storefront views are organized into the following categories:

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

### 1.2 Layout Hierarchy

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

### 1.3 Theme Settings

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

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

### 1.4 Section-Based Architecture

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

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

### 1.5 Blade Components

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

---

## 2. Base Layout

### 2.1 Document Head

The document head includes:

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

### 2.2 Skip Link

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

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

### 2.3 Announcement Bar

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

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

### 2.4 Header

The header serves as the primary navigation banner.

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

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

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

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


content:
/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Auth/Login.php
/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Auth/Logout.php
/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Dashboard.php


content:
/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Auth/Login.php
/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Auth/Register.php
/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Dashboard.php


content:
   150→
   151→```
   152→+--------------------------------------------------------------+
   153→| [Hamburger]           [Logo]                            [Cart]|
   154→+--------------------------------------------------------------+
   155→```
   156→
   157→| Element | Position | Description |
   158→|---|---|---|
   159→| Hamburger menu | Left | Toggle button for mobile navigation drawer |
   160→| Logo | Center | Same as desktop but horizontally centered |
   161→| Cart icon | Right | Same as desktop |
   162→
   163→**Sticky Header (optional):**
   164→
   165→When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.
   166→
   167→**Mobile Navigation Drawer:**
   168→
   169→| Attribute | Description |
   170→|---|---|
   171→| Animation | Slides in from the left side |
   172→| Height | Full viewport height |
   173→| Backdrop | Semi-transparent dark overlay behind the drawer |
   174→| Close button | X icon at the top-right of the drawer |
   175→| Navigation items | Vertical stack with comfortable tap targets |
   176→| Submenus | Collapsible accordion pattern with chevron icons |
   177→| Account link | Positioned at the bottom of the navigation list |
   178→| Focus behavior | Focus is trapped within the drawer while open |
   179→| ARIA | Labeled as "Mobile navigation" |
   180→
   181→### 2.5 Main Content Area
   182→
   183→The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.
   184→
   185→### 2.6 Footer
   186→
   187→The footer is a content-info landmark.
   188→
   189→**Layout:**
   190→
   191→```
   192→+--------------------------------------------------------------+
   193→| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
   194→|                                                               |
   195→| [Social Icons]                                                |
   196→| [Copyright] [Payment Icons]                                   |
   197→+--------------------------------------------------------------+
   198→```
   199→
   200→| Element | Description |
   201→|---|---|
   202→| Link columns | Responsive grid: 2 columns on mobile, 3 on tablet, up to 4 on desktop. Links sourced from "footer-menu" navigation menu. Each column has an uppercase heading and a list of links |
   203→| Store info column | Store name, address (if configured), contact email |
   204→| Social links | Row of icon links (Facebook, Instagram, Twitter/X, TikTok, YouTube) configured in theme settings. Each with screen reader label. Icons are 20x20, muted color with hover to full contrast |
   205→| Copyright line | Small muted text: "(c) {year} {store name}. All rights reserved." |
   206→| Payment method icons | Row of SVG icons for accepted payment methods (Visa, Mastercard, Amex, PayPal, etc.). Slightly reduced opacity |
   207→| Spacing | Generous vertical padding, content constrained to max-width container with horizontal padding |
   208→| Background | Light gray (dark: very dark gray), top border separator |
   209→
   210→### 2.7 Cart Drawer (Livewire)
   211→
   212→The cart drawer is always present in the layout as a global Livewire component. It listens for the "cart-updated" browser event to refresh and open. See Section 6 for full specification.
   213→
   214→### 2.8 Dark Mode
   215→
   216→Dark mode is implemented via Tailwind's dark variant. The behavior is controlled by theme settings:
   217→
   218→| Mode | Implementation |
   219→|---|---|
   220→| System preference (default) | Follows OS-level dark mode preference automatically |
   221→| Toggle | User-controlled toggle button in header/footer; preference stored in localStorage |
   222→| Forced light/dark | Applied at server render time on the HTML element |
   223→
   224→**Color Token Patterns:**
   225→
   226→| Token | Light Mode | Dark Mode |
   227→|---|---|---|
   228→| Page background | White | Near-black |
   229→| Card/section background | Light gray | Very dark gray |
   230→| Heading text | Near-black | White |
   231→| Body text | Medium gray | Light gray |
   232→| Borders | Light gray | Medium-dark gray |
   233→| Primary actions | Blue, darkens on hover | Slightly lighter blue, lightens on hover |
   234→
   235→---
   236→
   237→## 3. Home Page (GET /)
   238→
   239→The home page renders sections in the order defined by theme settings. Each section can be toggled on/off and reordered. Default section order:
   240→
   241→1. Hero Banner
   242→2. Featured Collections
   243→3. Featured Products
   244→4. Newsletter Signup
   245→5. Rich Text
   246→
   247→### 3.1 Hero Banner Section
   248→
   249→**Layout:** Full-width container (no max-width on the outer wrapper). Inner content constrained to standard max-width.
   250→
   251→**Desktop (large breakpoint and above):**
   252→
   253→```
   254→+--------------------------------------------------------------+
   255→|                                                               |
   256→|  [Background Image / Gradient]                                |
   257→|                                                               |
   258→|       [Heading - very large, bold]                            |
   259→|       [Subheading - medium, regular weight]                   |
   260→|       [CTA Button - primary, large]                           |
   261→|                                                               |
   262→+--------------------------------------------------------------+
   263→```
   264→
   265→| Attribute | Description |
   266→|---|---|
   267→| Background | Full-width image (cover, centered) or gradient. Image set via theme settings. Semi-transparent dark overlay for text readability |
   268→| Content | Centered vertically and horizontally, white text |
   269→| Heading | H1 or H2 depending on context. Very large on desktop, progressively smaller on mobile. Bold, tight tracking |
   270→| Subheading | Regular paragraph, medium size, generous top margin. Constrained to a comfortable reading width |
   271→| CTA | Styled as primary button, large. Links to a configured collection or product |
   272→| Height | Minimum 600px on desktop, 500px on tablet, 400px on mobile, 300px on small mobile |
   273→| Padding | Generous vertical and horizontal padding |
   274→
   275→**Mobile:** Same structure but stacked, with reduced font sizes and shorter minimum height. Full-bleed background image remains.
   276→
   277→**Accessibility:**
   278→
   279→- If background is an image, it is decorative (the content provides meaning)
   280→- CTA button has descriptive text (e.g., "Shop the summer collection")
   281→
   282→### 3.2 Featured Collections Section
   283→
   284→**Layout:** Section with heading and grid of collection cards.
   285→
   286→| Attribute | Description |
   287→|---|---|
   288→| Section heading | H2, large, bold, centered, with generous bottom margin |
   289→| Grid | 2 columns on mobile, 4 columns on large screens, with gap spacing |
   290→| Count | 2 to 4 collections, configurable in theme settings |
   291→
   292→**Collection Card:**
   293→
   294→```
   295→+---------------------------+
   296→|  [Collection Image]       |
   297→|  3:4 aspect ratio         |
   298→|  cover-fit, rounded       |
   299→|                           |
   300→|  [Title overlay at bottom]|
   301→|  [Shop now ->]            |
   302→+---------------------------+
   303→```
   304→
   305→| Element | Description |
   306→|---|---|
   307→| Image | 3:4 aspect ratio, cover-fit, rounded corners. Gradient overlay at bottom fading from transparent to semi-transparent dark |
   308→| Title | White text overlaid at bottom of image, semi-bold, medium-large size |
   309→| "Shop now" link | Small white text below title, slightly transparent, underlined. Becomes fully opaque on hover |
   310→| Interaction | Entire card is clickable. Subtle scale-up on hover with smooth transition |
   311→
   312→**Accessibility:**
   313→
   314→- Each card link has the collection title as accessible name
   315→- Images have alt text matching the collection title
   316→
   317→### 3.3 Featured Products Section
   318→
   319→**Livewire Component:** FeaturedProducts
   320→
   321→This section uses Livewire for lazy loading to improve initial page load.
   322→
   323→| Attribute | Description |
   324→|---|---|
   325→| Section heading | H2, same styling as featured collections heading |
   326→| Grid | 2 columns on mobile, 3 on tablet, 4 on desktop, with gap spacing |
   327→| Products | 4 to 8, configurable. Sourced from a configured collection or "best sellers" logic |
   328→| Loading state | Skeleton placeholders with pulsing animation, matching the product card dimensions |
   329→
   330→Each product uses the product-card component (see Section 16).
   331→
   332→### 3.4 Newsletter Signup Section
   333→
   334→**Livewire Component:** NewsletterSignup
   335→
   336→**Layout:**
   337→
   338→```
   339→+--------------------------------------------------------------+
   340→|  [Heading: "Stay in the loop"]                                |
   341→|  [Subtext: "Subscribe for exclusive offers and updates."]     |
   342→|  [Email input] [Subscribe button]                             |
   343→+--------------------------------------------------------------+
   344→```
   345→
   346→| Attribute | Description |
   347→|---|---|
   348→| Background | Light gray (dark mode: darker gray), generous vertical padding |
   349→| Content | Centered, constrained to a comfortable width |

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

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

**Sticky Header (optional):**

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

**Mobile Navigation Drawer:**

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

### 2.5 Main Content Area

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

### 2.6 Footer

The footer is a content-info landmark.

**Layout:**

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

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

### 2.7 Cart Drawer (Livewire)

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

### 2.8 Dark Mode

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

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

**Color Token Patterns:**

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

---

## 3. Home Page (GET /)

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

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

### 3.1 Hero Banner Section

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

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

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

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

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

**Accessibility:**

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

### 3.2 Featured Collections Section

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

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

**Collection Card:**

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

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

**Accessibility:**

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

### 3.3 Featured Products Section

**Livewire Component:** FeaturedProducts

This section uses Livewire for lazy loading to improve initial page load.

| Attribute | Description |
|---|---|
| Section heading | H2, same styling as featured collections heading |
| Grid | 2 columns on mobile, 3 on tablet, 4 on desktop, with gap spacing |
| Products | 4 to 8, configurable. Sourced from a configured collection or "best sellers" logic |
| Loading state | Skeleton placeholders with pulsing animation, matching the product card dimensions |

Each product uses the product-card component (see Section 16).

### 3.4 Newsletter Signup Section

**Livewire Component:** NewsletterSignup

**Layout:**

```
+--------------------------------------------------------------+
|  [Heading: "Stay in the loop"]                                |
|  [Subtext: "Subscribe for exclusive offers and updates."]     |
|  [Email input] [Subscribe button]                             |
+--------------------------------------------------------------+
```

| Attribute | Description |
|---|---|
| Background | Light gray (dark mode: darker gray), generous vertical padding |
| Content | Centered, constrained to a comfortable width |


content:
     1→<?php
     2→
     3→namespace App\Livewire\Storefront\Account\Auth;
     4→
     5→use Illuminate\Support\Facades\Auth;
     6→use Illuminate\Support\Facades\RateLimiter;
     7→use Illuminate\Support\Facades\Session;
     8→use Illuminate\Validation\ValidationException;
     9→use Livewire\Attributes\Layout;
    10→use Livewire\Component;
    11→
    12→#[Layout('layouts.auth')]
    13→class Login extends Component
    14→{
    15→    public string $email = '';
    16→
    17→    public string $password = '';
    18→
    19→    public bool $remember = false;
    20→
    21→    /**
    22→     * Attempt to authenticate the customer.
    23→     */
    24→    public function login(): void
    25→    {
    26→        $this->validate([
    27→            'email' => ['required', 'string', 'email'],
    28→            'password' => ['required', 'string'],
    29→        ]);
    30→
    31→        $this->ensureIsNotRateLimited();
    32→
    33→        if (! Auth::guard('customer')->attempt(
    34→            ['email' => $this->email, 'password' => $this->password],
    35→            $this->remember
    36→        )) {
    37→            RateLimiter::hit($this->throttleKey(), 60);
    38→
    39→            throw ValidationException::withMessages([
    40→                'email' => __('auth.failed'),
    41→            ]);
    42→        }
    43→
    44→        RateLimiter::clear($this->throttleKey());
    45→
    46→        Session::regenerate();
    47→
    48→        $this->redirect(route('customer.dashboard'), navigate: true);
    49→    }
    50→
    51→    /**
    52→     * Ensure the authentication request is not rate limited.
    53→     */
    54→    protected function ensureIsNotRateLimited(): void
    55→    {
    56→        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
    57→            return;
    58→        }
    59→
    60→        $seconds = RateLimiter::availableIn($this->throttleKey());
    61→
    62→        throw ValidationException::withMessages([
    63→            'email' => __('auth.throttle', ['seconds' => $seconds]),
    64→        ]);
    65→    }
    66→
    67→    /**
    68→     * Get the throttle key for the request.
    69→     */
    70→    protected function throttleKey(): string
    71→    {
    72→        return 'customer-login:'.request()->ip();
    73→    }
    74→
    75→    public function render(): \Illuminate\View\View
    76→    {
    77→        return view('livewire.storefront.account.auth.login');
    78→    }
    79→}
    80→

content:
<?php

namespace App\Livewire\Storefront\Account\Auth;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('layouts.auth')]
class Login extends Component
{
    public string $email = '';

    public string $password = '';

    public bool $remember = false;

    /**
     * Attempt to authenticate the customer.
     */
    public function login(): void
    {
        $this->validate([
            'email' => ['required', 'string', 'email'],
            'password' => ['required', 'string'],
        ]);

        $this->ensureIsNotRateLimited();

        if (! Auth::guard('customer')->attempt(
            ['email' => $this->email, 'password' => $this->password],
            $this->remember
        )) {
            RateLimiter::hit($this->throttleKey(), 60);

            throw ValidationException::withMessages([
                'email' => __('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());

        Session::regenerate();

        $this->redirect(route('customer.dashboard'), navigate: true);
    }

    /**
     * Ensure the authentication request is not rate limited.
     */
    protected function ensureIsNotRateLimited(): void
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => __('auth.throttle', ['seconds' => $seconds]),
        ]);
    }

    /**
     * Get the throttle key for the request.
     */
    protected function throttleKey(): string
    {
        return 'customer-login:'.request()->ip();
    }

    public function render(): \Illuminate\View\View
    {
        return view('livewire.storefront.account.auth.login');
    }
}


content:
     1→<?php
     2→
     3→namespace App\Livewire\Storefront\Account;
     4→
     5→use Livewire\Attributes\Layout;
     6→use Livewire\Component;
     7→
     8→#[Layout('layouts.app')]
     9→class Dashboard extends Component
    10→{
    11→    public function render(): \Illuminate\View\View
    12→    {
    13→        return view('livewire.storefront.account.dashboard');
    14→    }
    15→}
    16→

content:
<?php

namespace App\Livewire\Storefront\Account;

use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('layouts.app')]
class Dashboard extends Component
{
    public function render(): \Illuminate\View\View
    {
        return view('livewire.storefront.account.dashboard');
    }
}


content:
     1→<?php
     2→
     3→namespace App\Livewire\Admin;
     4→
     5→use Livewire\Attributes\Layout;
     6→use Livewire\Component;
     7→
     8→#[Layout('layouts.app')]
     9→class Dashboard extends Component
    10→{
    11→    public function render(): \Illuminate\View\View
    12→    {
    13→        return view('livewire.admin.dashboard');
    14→    }
    15→}
    16→

content:
<?php

namespace App\Livewire\Admin;

use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('layouts.app')]
class Dashboard extends Component
{
    public function render(): \Illuminate\View\View
    {
        return view('livewire.admin.dashboard');
    }
}


text: Good. I have a clear picture of the current state. Let me now launch Phase 6, Phase 7, and Phase 9+10 as parallel sub-agents.