command: sed -n '1100,1660p' specs/09-IMPLEMENTATION-ROADMAP.md

stdout:
| detects version mismatch | cart at version 3, client sends expected_version 2 | CartVersionMismatchException thrown | validation |

---

#### tests/Unit/HandleGeneratorTest.php

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

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

---

### Feature Tests

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a cart via API | POST /api/storefront/v1/carts | 201 response, response contains cart data | happy path |
| retrieves a cart via API | create cart, then GET /api/storefront/v1/carts/{id} | 200 response, cart data with lines and totals | happy path |
| adds a line via API | POST /api/storefront/v1/carts/{id}/lines with variant_id and quantity | 200 response, line added to cart | happy path |
| updates line quantity via API | PUT /api/storefront/v1/carts/{id}/lines/{lineId} with quantity | 200 response, quantity updated | happy path |
| removes a line via API | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
| returns 404 for nonexistent cart | GET /api/storefront/v1/carts/nonexistent | 404 response | validation |
| returns 409 on version mismatch | cart at version 3; PUT with expected_version=2 | 409 Conflict with current cart state | validation |
| respects storefront rate limiting | make 121 requests in 1 minute | 121st returns 429 | validation |

---

#### tests/Feature/Checkout/CheckoutFlowTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a checkout from a cart | cart with 2 lines; create checkout | checkout in DB, status=started, linked to cart | happy path |
| completes full checkout happy path | create cart -> add lines -> create checkout -> set address -> select shipping -> begin payment -> complete | order created, cart status=converted, inventory committed | happy path |
| rejects checkout for empty cart | cart with 0 lines; create checkout | validation error | validation |
| expires checkout after timeout | checkout with payment_selected status, expires_at in the past; run ExpireAbandonedCheckouts job | status=expired, inventory released | edge case |
| prevents duplicate orders from same checkout | complete checkout once (creates order); call completeCheckout again | no duplicate order created, same order returned | edge case |

---

#### tests/Feature/Checkout/CheckoutStateTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| transitions from started to addressed with valid address | set email and shipping address | status = addressed | happy path |
| rejects address transition with missing required fields | set address without city | validation error, status remains started | validation |
| transitions from addressed to shipping_selected | set valid shipping rate | status = shipping_selected | happy path |
| rejects shipping selection with rate from wrong zone | address in DE, select rate from US-only zone | exception thrown | validation |
| skips shipping selection when no items require shipping | all variants have requires_shipping=false | transitions directly to shipping_selected with shipping=0 | edge case |
| transitions from shipping_selected to payment_selected | select payment method | status = payment_selected, expires_at set, inventory reserved | happy path |
| transitions from payment_selected to completed | simulate successful payment | status = completed, order created | happy path |
| rejects invalid state transitions | attempt started -> completed | exception thrown | validation |
| recalculates pricing on address change | set address (DE), note tax calculation; change address to different country | pricing recalculated | happy path |

---

#### tests/Feature/Checkout/PricingIntegrationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates correct totals for a simple checkout | 1 item: 2500 x 2 = 5000, no discount, flat shipping 499, 19% tax exclusive | subtotal=5000, shipping=499, tax=1044 (19% of 5499), total=6543 | happy path |
| applies discount code and recalculates | apply 10% discount to 10000 subtotal | discount=1000, discounted subtotal=9000 | happy path |
| stores pricing snapshot in totals_json | complete pricing calculation | checkout.totals_json contains all fields | happy path |
| recalculates on shipping method change | select flat rate 499, then switch to weight-based 899 | totals updated with new shipping amount | happy path |
| handles prices-include-tax correctly | tax settings: prices_include_tax=true, rate=1900; item price 11900 (gross) | tax extracted = 1900, subtotal = 10000 | edge case |

---

#### tests/Feature/Checkout/DiscountTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| applies a valid percent discount code at checkout | discount: 10%, code "SAVE10"; apply to checkout with subtotal 5000 | discount = 500 | happy path |
| applies a valid fixed discount code at checkout | discount: 500 fixed, code "5OFF" | discount = 500 | happy path |
| removes discount when code is cleared | apply discount, then clear code | discount = 0, totals recalculated | happy path |
| rejects expired discount at checkout | discount ended yesterday | error returned | validation |
| increments usage count on order completion | discount usage_count=5; complete checkout with discount | usage_count=6 | happy path |
| handles free shipping discount at checkout | apply free_shipping discount | shipping = 0 in totals | happy path |

---

#### tests/Feature/Checkout/ShippingTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| returns available shipping rates for address | store has zone for DE with flat rate 499; address: country DE | rate returned with name and amount | happy path |
| returns empty when no zone matches address | store has zone for DE only; address: country FR | empty rates | edge case |
| calculates flat rate correctly | select flat rate, amount 499 | shipping in totals = 499 | happy path |
| calculates weight-based rate correctly | cart total weight 750g, rate range 501-2000g = 899 | shipping = 899 | happy path |
| returns zero shipping when all items are digital | all variants: requires_shipping=false | shipping = 0 | edge case |

---

#### tests/Feature/Checkout/TaxTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| calculates exclusive tax correctly at checkout | tax mode: manual, rate: 1900, prices_include_tax: false; discounted subtotal + shipping = 5499 | tax = 1044 (rounded) | happy path |
| extracts inclusive tax correctly at checkout | tax mode: manual, rate: 1900, prices_include_tax: true | tax extracted from gross amounts | happy path |
| applies zero tax when no tax settings exist | no tax_settings row for store | tax = 0 | edge case |
| stores tax lines in totals_json | calculate with rate 1900 | tax_lines array in totals_json has entry with name, rate, amount | happy path |

---

#### tests/Feature/Orders/OrderCreationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates an order from a completed checkout | complete checkout flow | order in DB with correct amounts, status=pending | happy path |
| generates sequential order numbers per store | create 3 orders in store A | order numbers are #1001, #1002, #1003 | happy path |
| creates order lines with snapshots | order from checkout with 2 items | order_lines have title_snapshot, sku_snapshot | happy path |
| commits inventory on order creation | variant on_hand=10, reserved=3; create order with qty 3 | on_hand=7, reserved=0 | happy path |
| marks cart as converted | create order from checkout | cart status = converted | happy path |
| dispatches OrderCreated event | use Event::fake(); create order | OrderCreated dispatched with order | happy path |
| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
| links order to customer when authenticated | checkout by logged-in customer | order.customer_id set | happy path |
| sets email from checkout on the order | guest checkout with email "test@example.com" | order.email = "test@example.com" | happy path |

---

#### tests/Feature/Orders/RefundTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a full refund | order with total 5000, payment captured; refund 5000 | refund in DB, financial_status = refunded | happy path |
| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
| rejects refund exceeding payment amount | payment amount 5000; attempt to refund 6000 | validation error | validation |
| restocks inventory when restock flag is true | refund with restock=true, qty 2 | inventory on_hand increased by 2 | happy path |
| does not restock when restock flag is false | refund with restock=false | inventory unchanged | edge case |
| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
| records refund reason | refund with reason "Customer requested" | refund.reason = "Customer requested" | happy path |

---

#### tests/Feature/Orders/FulfillmentTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a fulfillment for specific order lines | order with 2 lines (qty 3 and qty 2); fulfill line 1 qty 3 | fulfillment created, fulfillment_lines has 1 row | happy path |
| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
| updates order fulfillment status to fulfilled when all lines done | fulfill all remaining lines | order.fulfillment_status = fulfilled | happy path |
| adds tracking information | ship fulfillment with tracking_company="DHL", tracking_number="123456" | tracking info saved, shipped_at set | happy path |
| transitions fulfillment from pending to shipped | ship fulfillment | status = shipped | happy path |
| transitions fulfillment from shipped to delivered | deliver fulfillment | status = delivered | happy path |
| prevents fulfilling more than ordered quantity | order line qty 2; attempt to fulfill qty 3 | validation error | validation |
| fulfillment guard blocks fulfillment when financial_status is pending | order with financial_status=pending (bank transfer) | FulfillmentGuardException thrown | validation |
| fulfillment guard allows fulfillment when financial_status is paid | order with financial_status=paid | fulfillment created successfully | happy path |
| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |

---

#### tests/Feature/Payments/MockPaymentProviderTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
| generates mock reference ID | any charge | provider_payment_id starts with "mock_" | happy path |

---

#### tests/Feature/Payments/PaymentServiceTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| processes credit card payment and creates order as paid | checkout with credit card | order created with financial_status=paid, inventory committed | happy path |
| processes PayPal payment and creates order as paid | checkout with PayPal | order created with financial_status=paid, inventory committed | happy path |
| processes bank transfer and creates order as pending | checkout with bank transfer | order created with financial_status=pending, inventory reserved but not committed | happy path |
| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
| creates a payment record with correct method | charge with credit_card | payment row in DB with method=credit_card, provider=mock | happy path |

---

#### tests/Feature/Payments/BankTransferConfirmationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| admin can confirm bank transfer payment | order with payment_method=bank_transfer, financial_status=pending | financial_status transitions to paid, inventory committed | happy path |
| cannot confirm payment for non-bank-transfer orders | order with payment_method=credit_card | error thrown | validation |
| cannot confirm already confirmed payment | order with financial_status=paid | error thrown | validation |
| auto-cancel job cancels unpaid bank transfer orders after config days | order with payment_method=bank_transfer, placed_at > 7 days ago | order cancelled, inventory released | happy path |
| auto-cancel job does not cancel orders within config days | order with payment_method=bank_transfer, placed_at = 2 days ago | order remains pending | edge case |
| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |

---

#### tests/Feature/Customers/CustomerAccountTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the customer dashboard | acting as customer; GET /account | 200 response, sees customer name | happy path |
| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
| redirects unauthenticated requests to login | GET /account without auth | redirected to /account/login | authorization |
| updates customer profile | update name and marketing_opt_in | changes persisted in DB | happy path |

---

#### tests/Feature/Customers/AddressManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists saved addresses | customer with 2 addresses; GET /account/addresses | sees both addresses | happy path |
| creates a new address | submit address form | address saved in DB | happy path |
| updates an existing address | change city on existing address | updated in DB | happy path |
| deletes an address | delete address | removed from DB | happy path |
| sets a default address | mark address as default | is_default=1, other addresses is_default=0 | happy path |
| validates required address fields | submit with missing address1 | validation error | validation |
| prevents managing another customers addresses | customer A logged in, attempt to edit customer B address | 403 or 404 response | authorization |

---

#### tests/Feature/Admin/DashboardTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the admin dashboard | acting as admin; GET /admin | 200 response, sees "Dashboard" | happy path |
| shows KPI tiles with correct data | store has 5 orders totaling 25000 cents | sees order count 5, total revenue formatted | happy path |
| restricts dashboard to authenticated admins | unauthenticated GET /admin | redirected to /admin/login | authorization |
| filters metrics by date range | orders across multiple dates; filter to last 7 days | only matching orders included in KPIs | happy path |

---

#### tests/Feature/Admin/ProductManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists products with pagination | create 25 products; GET /admin/products | first page shows 15 (or configured per-page), pagination controls visible | happy path |
| creates a product via admin form | Livewire test: fill title, description, submit | product in DB | happy path |
| edits a product via admin form | Livewire test: change title, submit | product updated | happy path |
| bulk archives selected products | select 3 products, trigger bulk archive | all 3 status = archived | happy path |
| uploads media from the product form | Livewire file upload on product edit | media attached to product | happy path |
| manages variants from the product form | add options and values via Livewire | variants generated | happy path |
| restricts product management to authorized roles | support user GET /admin/products: can see list (read-only); support user attempt to create product | 403 on create | authorization |
| staff can create but not delete products | staff user: create product succeeds; staff user: delete product | fails with 403 on delete | authorization |

---

#### tests/Feature/Admin/OrderManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists orders with status filter | 3 pending, 2 paid orders; filter by status=paid | 2 results | happy path |
| shows order detail page | GET /admin/orders/{order} | sees order number, line items, totals | happy path |
| creates a fulfillment from order detail | Livewire action: select lines, submit fulfillment | fulfillment created | happy path |
| processes a refund from order detail | Livewire action: enter amount, submit refund | refund created | happy path |
| restricts order management by role | support user can view orders (read-only); support user cannot update or fulfill | appropriate 403 responses | authorization |

---

#### tests/Feature/Admin/DiscountManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists discounts | create 3 discounts; GET /admin/discounts | sees all 3 | happy path |
| creates a percent discount | Livewire: code="SAVE10", type=percent, value=10 | discount in DB | happy path |
| creates a fixed discount | Livewire: code="5OFF", type=fixed, value=500 | discount in DB | happy path |
| validates discount code uniqueness within store | create discount with code "SAVE10"; attempt to create another with same code | validation error | validation |
| edits a discount | change value from 10 to 15 | updated in DB | happy path |
| disables a discount | set status to disabled | status = disabled | happy path |

---

#### tests/Feature/Admin/SettingsTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the settings page | GET /admin/settings | 200 response, sees settings tabs | happy path |
| updates general store settings | update store name | store_settings updated | happy path |
| configures shipping zones | create zone with countries and rates | shipping_zone and shipping_rates in DB | happy path |
| configures tax settings | set mode=manual, rate=1900, prices_include_tax=true | tax_settings updated | happy path |
| restricts settings to owner and admin roles | staff user GET /admin/settings | 403 response | authorization |
| manages store domains | add a new domain | store_domain created | happy path |

---

#### tests/Feature/Search/SearchTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| returns products matching search query | products: "Blue Cotton T-Shirt", "Red Wool Sweater"; search "cotton" | finds "Blue Cotton T-Shirt" | happy path |
| scopes search to current store | store A has "T-Shirt", store B has "T-Shirt Deluxe"; search "t-shirt" on store A | only store A product returned | authorization |
| returns empty for no matches | search "xyznonexistent" | empty results | edge case |
| logs search query for analytics | perform search | search_queries row created | happy path |
| paginates search results | 25 matching products, 12 per page | page 1 has 12, page 2 has 12, page 3 has 1 | happy path |

---

#### tests/Feature/Search/AutocompleteTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| returns suggestions matching prefix | products: "Summer Dress", "Summer Hat", "Winter Coat"; autocomplete prefix "sum" | "Summer Dress" and "Summer Hat" returned | happy path |
| limits results to configured count | 20 matching products, limit 5 | 5 results returned | happy path |
| returns empty for very short prefix | autocomplete prefix "a" (below minimum) | empty or all results | edge case |

---

#### tests/Feature/Analytics/EventIngestionTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| tracks a page view event | POST analytics event: type=page_view | analytics_events row created | happy path |
| tracks an add to cart event | POST event: type=add_to_cart, properties include product_id | event stored with properties | happy path |
| scopes events to current store | track event on store A | event.store_id = store A id | happy path |
| includes session ID when available | track event with session | session_id stored | happy path |
| includes customer ID when authenticated | track event as logged-in customer | customer_id stored | happy path |

---

#### tests/Feature/Analytics/AggregationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| aggregates daily metrics from raw events | create events: 5 page_views, 3 add_to_carts, 2 checkout_completed; run AggregateAnalytics job | analytics_daily row with correct counts | happy path |
| calculates revenue and AOV correctly | 3 orders: 1000, 2000, 3000 cents; aggregate | revenue=6000, aov=2000 | happy path |
| runs idempotently | run aggregation twice for same date | analytics_daily row has correct values (not doubled) | edge case |

---

#### tests/Feature/Webhooks/WebhookDeliveryTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| delivers a webhook to a subscribed URL | mock HTTP client; subscription for event_type "order.created"; dispatch event | HTTP POST made to target_url | happy path |
| signs the payload with HMAC | deliver webhook | `X-Platform-Signature` header present with valid signature | happy path |
| retries failed deliveries with exponential backoff | first delivery returns 500 | job retried, attempt_count incremented | edge case |
| marks delivery as failed after max retries | 6 total attempts exhausted (1 initial + 5 retries) | delivery status = failed | edge case |
| pauses subscription after circuit breaker threshold | 5 consecutive failures | subscription status = paused | edge case |

---

#### tests/Feature/Webhooks/WebhookSignatureTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| generates a valid HMAC-SHA256 signature | payload: '{"event":"order.created"}'; secret: 'test-secret' | signature matches expected hash | happy path |
| verifies a valid signature | generate signature, then verify | returns true | happy path |
| rejects a tampered payload | sign original, verify with modified payload | returns false | validation |
| rejects an incorrect secret | sign with secret A, verify with secret B | returns false | validation |

---

#### tests/Feature/Api/StorefrontCartApiTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a cart | POST /api/storefront/v1/carts | 201 response, body has cart id, version 1 | happy path |
| retrieves a cart with lines and totals | create cart with 2 lines; GET /api/storefront/v1/carts/{id} | 200 response, lines array has 2 items, totals calculated | happy path |
| adds a line to the cart | POST /api/storefront/v1/carts/{id}/lines | 200 response, line added | happy path |
| updates a line quantity | PUT /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, quantity updated | happy path |
| removes a line | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
| validates variant exists on add | add nonexistent variant_id | 422 response | validation |
| validates quantity is positive | add with quantity=0 | 422 response | validation |
| returns 404 for nonexistent cart | GET /api/storefront/v1/carts/999 | 404 response | validation |

---

#### tests/Feature/Api/StorefrontCheckoutApiTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a checkout from a cart | POST /api/storefront/v1/checkouts with cart_id | 201 response, checkout status=started | happy path |
| sets checkout address | PUT /api/storefront/v1/checkouts/{id}/address | 200 response, status=addressed | happy path |
| selects a shipping method | PUT /api/storefront/v1/checkouts/{id}/shipping-method | 200 response, status=shipping_selected, available rates returned in response | happy path |
| applies a discount code | POST /api/storefront/v1/checkouts/{id}/apply-discount with code | 200 response, discount applied in totals | happy path |
| retrieves checkout with totals | GET /api/storefront/v1/checkouts/{id} | 200 response, includes totals_json fields | happy path |
| selects a payment method | PUT /api/storefront/v1/checkouts/{id}/payment-method with method=credit_card | 200 response, status=payment_selected, payment_method stored | happy path |
| completes checkout with credit card payment | POST /api/storefront/v1/checkouts/{id}/pay with card fields | 200 response, status=completed, order created | happy path |
| rejects payment with declined card | POST /api/storefront/v1/checkouts/{id}/pay with magic decline number | 422 response, payment declined message | validation |
| validates required address fields | PUT address with missing city | 422 response | validation |

formatted_output:
Warning: truncated output (original token count: 11097)
Total output lines: 561

| detects version mismatch | cart at version 3, client sends expected_version 2 | CartVersionMismatchException thrown | validation |

---

#### tests/Unit/HandleGeneratorTest.php

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

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

---

### Feature Tests

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

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

---

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

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a cart via API | POST /api/storefront/v1/carts | 201 response, response contains cart data | happy path |
| retrieves a cart via API | create cart, then GET /api/storefront/v1/carts/{id} | 200 response, cart data with lines and totals | happy path |
| adds a line via API | POST /api/storefront/v1/carts/{id}/lines with variant_id and quantity | 200 response, line added to cart | happy path |
| updates line quantity via API | PUT /api/storefront/v1/carts/{id}/lines/{lineId} with quantity | 200 response, quantity updated | happy path |
| removes a line via API | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
| returns 404 for nonexistent cart | GET /api/storefront/v1/carts/nonexistent | 404 response | validation |
| returns 409 on version mismatch | cart at version 3; PUT with expected_version=2 | 409 Conflict with current cart state | validation |
| respects storefront rate limiting | make 121 requests in 1 minute | 121st returns 429 | validation |

---

#### tests/Feature/Checkout/CheckoutFlowTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a checkout from a cart | cart with 2 lines; create checkout | checkout in DB, status=started, linked to cart | happy path |
| completes full checkout happy path | create cart -> add lines -> create checkout -> set address -> select shipping -> begin payment -> complete | order created, cart status=converted, inventory committed | happy path |
| rejects checkout for empty cart | cart with 0 lines; create checkout | validation error | validation |
| expires checkout after timeout | checkout with payment_selected status, expires_at in the past; run ExpireAbandonedCheckouts job | status=expired, inventory released | edge case |
| prevents duplicate orders from same checkout | complete checkout once (creates order); call completeCheckout again | no duplicate order created, same order returned | edge case |

---

#### tests/Feature/Checkout/CheckoutStateTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| transitions from started to addressed with valid address | set email and shipping address | status = addressed | happy path |
| rejects address transition with missing required fields | set address without city | validation error, status remains started | validation |
| transitions from addressed to shipping_selected | set valid shipping rate | status = shipping_selected | happy path |
| rejects shipping selection with rate from wrong zone | address in DE, select rate from US-only zone | exception thrown | validation |
| skips shipping selection when no items require shipping | all variants have requires_shipping=false | transitions directly to shipping_selected with shipping=0 | edge case |
| transitions from shipping_selected to payment_selected | select payment method | status = payment_selected, expires_at set, inventory reserved | happy path |
| transitions from payment_selected to completed | simulate successful payment | status = completed, order created | happy path |
| rejects invalid state transitions | attempt start…1097 tokens truncated…qty 3 | on_hand=7, reserved=0 | happy path |
| marks cart as converted | create order from checkout | cart status = converted | happy path |
| dispatches OrderCreated event | use Event::fake(); create order | OrderCreated dispatched with order | happy path |
| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
| links order to customer when authenticated | checkout by logged-in customer | order.customer_id set | happy path |
| sets email from checkout on the order | guest checkout with email "test@example.com" | order.email = "test@example.com" | happy path |

---

#### tests/Feature/Orders/RefundTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a full refund | order with total 5000, payment captured; refund 5000 | refund in DB, financial_status = refunded | happy path |
| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
| rejects refund exceeding payment amount | payment amount 5000; attempt to refund 6000 | validation error | validation |
| restocks inventory when restock flag is true | refund with restock=true, qty 2 | inventory on_hand increased by 2 | happy path |
| does not restock when restock flag is false | refund with restock=false | inventory unchanged | edge case |
| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
| records refund reason | refund with reason "Customer requested" | refund.reason = "Customer requested" | happy path |

---

#### tests/Feature/Orders/FulfillmentTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a fulfillment for specific order lines | order with 2 lines (qty 3 and qty 2); fulfill line 1 qty 3 | fulfillment created, fulfillment_lines has 1 row | happy path |
| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
| updates order fulfillment status to fulfilled when all lines done | fulfill all remaining lines | order.fulfillment_status = fulfilled | happy path |
| adds tracking information | ship fulfillment with tracking_company="DHL", tracking_number="123456" | tracking info saved, shipped_at set | happy path |
| transitions fulfillment from pending to shipped | ship fulfillment | status = shipped | happy path |
| transitions fulfillment from shipped to delivered | deliver fulfillment | status = delivered | happy path |
| prevents fulfilling more than ordered quantity | order line qty 2; attempt to fulfill qty 3 | validation error | validation |
| fulfillment guard blocks fulfillment when financial_status is pending | order with financial_status=pending (bank transfer) | FulfillmentGuardException thrown | validation |
| fulfillment guard allows fulfillment when financial_status is paid | order with financial_status=paid | fulfillment created successfully | happy path |
| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
| auto-fulfills digital products on payment confirmation | order with all digital items, financial_status transitions to paid | fulfillment auto-created with status=delivered | happy path |
| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |

---

#### tests/Feature/Payments/MockPaymentProviderTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
| generates mock reference ID | any charge | provider_payment_id starts with "mock_" | happy path |

---

#### tests/Feature/Payments/PaymentServiceTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| processes credit card payment and creates order as paid | checkout with credit card | order created with financial_status=paid, inventory committed | happy path |
| processes PayPal payment and creates order as paid | checkout with PayPal | order created with financial_status=paid, inventory committed | happy path |
| processes bank transfer and creates order as pending | checkout with bank transfer | order created with financial_status=pending, inventory reserved but not committed | happy path |
| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
| creates a payment record with correct method | charge with credit_card | payment row in DB with method=credit_card, provider=mock | happy path |

---

#### tests/Feature/Payments/BankTransferConfirmationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| admin can confirm bank transfer payment | order with payment_method=bank_transfer, financial_status=pending | financial_status transitions to paid, inventory committed | happy path |
| cannot confirm payment for non-bank-transfer orders | order with payment_method=credit_card | error thrown | validation |
| cannot confirm already confirmed payment | order with financial_status=paid | error thrown | validation |
| auto-cancel job cancels unpaid bank transfer orders after config days | order with payment_method=bank_transfer, placed_at > 7 days ago | order cancelled, inventory released | happy path |
| auto-cancel job does not cancel orders within config days | order with payment_method=bank_transfer, placed_at = 2 days ago | order remains pending | edge case |
| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |

---

#### tests/Feature/Customers/CustomerAccountTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the customer dashboard | acting as customer; GET /account | 200 response, sees customer name | happy path |
| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
| redirects unauthenticated requests to login | GET /account without auth | redirected to /account/login | authorization |
| updates customer profile | update name and marketing_opt_in | changes persisted in DB | happy path |

---

#### tests/Feature/Customers/AddressManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists saved addresses | customer with 2 addresses; GET /account/addresses | sees both addresses | happy path |
| creates a new address | submit address form | address saved in DB | happy path |
| updates an existing address | change city on existing address | updated in DB | happy path |
| deletes an address | delete address | removed from DB | happy path |
| sets a default address | mark address as default | is_default=1, other addresses is_default=0 | happy path |
| validates required address fields | submit with missing address1 | validation error | validation |
| prevents managing another customers addresses | customer A logged in, attempt to edit customer B address | 403 or 404 response | authorization |

---

#### tests/Feature/Admin/DashboardTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the admin dashboard | acting as admin; GET /admin | 200 response, sees "Dashboard" | happy path |
| shows KPI tiles with correct data | store has 5 orders totaling 25000 cents | sees order count 5, total revenue formatted | happy path |
| restricts dashboard to authenticated admins | unauthenticated GET /admin | redirected to /admin/login | authorization |
| filters metrics by date range | orders across multiple dates; filter to last 7 days | only matching orders included in KPIs | happy path |

---

#### tests/Feature/Admin/ProductManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists products with pagination | create 25 products; GET /admin/products | first page shows 15 (or configured per-page), pagination controls visible | happy path |
| creates a product via admin form | Livewire test: fill title, description, submit | product in DB | happy path |
| edits a product via admin form | Livewire test: change title, submit | product updated | happy path |
| bulk archives selected products | select 3 products, trigger bulk archive | all 3 status = archived | happy path |
| uploads media from the product form | Livewire file upload on product edit | media attached to product | happy path |
| manages variants from the product form | add options and values via Livewire | variants generated | happy path |
| restricts product management to authorized roles | support user GET /admin/products: can see list (read-only); support user attempt to create product | 403 on create | authorization |
| staff can create but not delete products | staff user: create product succeeds; staff user: delete product | fails with 403 on delete | authorization |

---

#### tests/Feature/Admin/OrderManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists orders with status filter | 3 pending, 2 paid orders; filter by status=paid | 2 results | happy path |
| shows order detail page | GET /admin/orders/{order} | sees order number, line items, totals | happy path |
| creates a fulfillment from order detail | Livewire action: select lines, submit fulfillment | fulfillment created | happy path |
| processes a refund from order detail | Livewire action: enter amount, submit refund | refund created | happy path |
| restricts order management by role | support user can view orders (read-only); support user cannot update or fulfill | appropriate 403 responses | authorization |

---

#### tests/Feature/Admin/DiscountManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| lists discounts | create 3 discounts; GET /admin/discounts | sees all 3 | happy path |
| creates a percent discount | Livewire: code="SAVE10", type=percent, value=10 | discount in DB | happy path |
| creates a fixed discount | Livewire: code="5OFF", type=fixed, value=500 | discount in DB | happy path |
| validates discount code uniqueness within store | create discount with code "SAVE10"; attempt to create another with same code | validation error | validation |
| edits a discount | change value from 10 to 15 | updated in DB | happy path |
| disables a discount | set status to disabled | status = disabled | happy path |

---

#### tests/Feature/Admin/SettingsTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the settings page | GET /admin/settings | 200 response, sees settings tabs | happy path |
| updates general store settings | update store name | store_settings updated | happy path |
| configures shipping zones | create zone with countries and rates | shipping_zone and shipping_rates in DB | happy path |
| configures tax settings | set mode=manual, rate=1900, prices_include_tax=true | tax_settings updated | happy path |
| restricts settings to owner and admin roles | staff user GET /admin/settings | 403 response | authorization |
| manages store domains | add a new domain | store_domain created | happy path |

---

#### tests/Feature/Search/SearchTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| returns products matching search query | products: "Blue Cotton T-Shirt", "Red Wool Sweater"; search "cotton" | finds "Blue Cotton T-Shirt" | happy path |
| scopes search to current store | store A has "T-Shirt", store B has "T-Shirt Deluxe"; search "t-shirt" on store A | only store A product returned | authorization |
| returns empty for no matches | search "xyznonexistent" | empty results | edge case |
| logs search query for analytics | perform search | search_queries row created | happy path |
| paginates search results | 25 matching products, 12 per page | page 1 has 12, page 2 has 12, page 3 has 1 | happy path |

---

#### tests/Feature/Search/AutocompleteTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| returns suggestions matching prefix | products: "Summer Dress", "Summer Hat", "Winter Coat"; autocomplete prefix "sum" | "Summer Dress" and "Summer Hat" returned | happy path |
| limits results to configured count | 20 matching products, limit 5 | 5 results returned | happy path |
| returns empty for very short prefix | autocomplete prefix "a" (below minimum) | empty or all results | edge case |

---

#### tests/Feature/Analytics/EventIngestionTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| tracks a page view event | POST analytics event: type=page_view | analytics_events row created | happy path |
| tracks an add to cart event | POST event: type=add_to_cart, properties include product_id | event stored with properties | happy path |
| scopes events to current store | track event on store A | event.store_id = store A id | happy path |
| includes session ID when available | track event with session | session_id stored | happy path |
| includes customer ID when authenticated | track event as logged-in customer | customer_id stored | happy path |

---

#### tests/Feature/Analytics/AggregationTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| aggregates daily metrics from raw events | create events: 5 page_views, 3 add_to_carts, 2 checkout_completed; run AggregateAnalytics job | analytics_daily row with correct counts | happy path |
| calculates revenue and AOV correctly | 3 orders: 1000, 2000, 3000 cents; aggregate | revenue=6000, aov=2000 | happy path |
| runs idempotently | run aggregation twice for same date | analytics_daily row has correct values (not doubled) | edge case |

---

#### tests/Feature/Webhooks/WebhookDeliveryTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| delivers a webhook to a subscribed URL | mock HTTP client; subscription for event_type "order.created"; dispatch event | HTTP POST made to target_url | happy path |
| signs the payload with HMAC | deliver webhook | `X-Platform-Signature` header present with valid signature | happy path |
| retries failed deliveries with exponential backoff | first delivery returns 500 | job retried, attempt_count incremented | edge case |
| marks delivery as failed after max retries | 6 total attempts exhausted (1 initial + 5 retries) | delivery status = failed | edge case |
| pauses subscription after circuit breaker threshold | 5 consecutive failures | subscription status = paused | edge case |

---

#### tests/Feature/Webhooks/WebhookSignatureTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| generates a valid HMAC-SHA256 signature | payload: '{"event":"order.created"}'; secret: 'test-secret' | signature matches expected hash | happy path |
| verifies a valid signature | generate signature, then verify | returns true | happy path |
| rejects a tampered payload | sign original, verify with modified payload | returns false | validation |
| rejects an incorrect secret | sign with secret A, verify with secret B | returns false | validation |

---

#### tests/Feature/Api/StorefrontCartApiTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a cart | POST /api/storefront/v1/carts | 201 response, body has cart id, version 1 | happy path |
| retrieves a cart with lines and totals | create cart with 2 lines; GET /api/storefront/v1/carts/{id} | 200 response, lines array has 2 items, totals calculated | happy path |
| adds a line to the cart | POST /api/storefront/v1/carts/{id}/lines | 200 response, line added | happy path |
| updates a line quantity | PUT /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, quantity updated | happy path |
| removes a line | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
| validates variant exists on add | add nonexistent variant_id | 422 response | validation |
| validates quantity is positive | add with quantity=0 | 422 response | validation |
| returns 404 for nonexistent cart | GET /api/storefront/v1/carts/999 | 404 response | validation |

---

#### tests/Feature/Api/StorefrontCheckoutApiTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| creates a checkout from a cart | POST /api/storefront/v1/checkouts with cart_id | 201 response, checkout status=started | happy path |
| sets checkout address | PUT /api/storefront/v1/checkouts/{id}/address | 200 response, status=addressed | happy path |
| selects a shipping method | PUT /api/storefront/v1/checkouts/{id}/shipping-method | 200 response, status=shipping_selected, available rates returned in response | happy path |
| applies a discount code | POST /api/storefront/v1/checkouts/{id}/apply-discount with code | 200 response, discount applied in totals | happy path |
| retrieves checkout with totals | GET /api/storefront/v1/checkouts/{id} | 200 response, includes totals_json fields | happy path |
| selects a payment method | PUT /api/storefront/v1/checkouts/{id}/payment-method with method=credit_card | 200 response, status=payment_selected, payment_method stored | happy path |
| completes checkout with credit card payment | POST /api/storefront/v1/checkouts/{id}/pay with card fields | 200 response, status=completed, order created | happy path |
| rejects payment with declined card | POST /api/storefront/v1/checkouts/{id}/pay with magic decline number | 422 response, payment declined message | validation |
| validates required address fields | PUT address with missing city | 422 response | validation |