content:
  1200→
  1201→#### tests/Feature/Products/ProductCrudTest.php
  1202→
  1203→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1204→|-----------|----------------------|----------------|----------|
  1205→| lists products for the current store | create 5 products for current store; GET /admin/products | 200 response, sees all 5 product titles | happy path |
  1206→| creates a product with a default variant | POST via Livewire action: title, description, status=draft | product in DB, one variant with is_default=1, inventory_item created | happy path |
  1207→| generates a unique handle from the title | create product with title "Summer T-Shirt" | handle = "summer-t-shirt" | happy path |
  1208→| appends suffix when handle collides | create two products with title "T-Shirt" | handles are "t-shirt" and "t-shirt-1" | edge case |
  1209→| updates a product | create product, then update title and description | DB reflects changes | happy path |
  1210→| transitions product from draft to active | product in draft with valid variant; transition to active | status = active, published_at set | happy path |
  1211→| rejects draft to active without a priced variant | product in draft, variant price = 0; transition to active | exception thrown, status unchanged | validation |
  1212→| transitions product from active to archived | active product; transition to archived | status = archived | happy path |
  1213→| prevents active to draft when order lines exist | active product with order references; transition to draft | exception thrown | validation |
  1214→| hard deletes a draft product with no order references | draft product, no orders; delete | product removed from DB | happy path |
  1215→| prevents deletion of product with order references | product with order_lines referencing its variants; attempt delete | exception thrown | validation |
  1216→| filters products by status | 3 active, 2 draft, 1 archived; filter by status=active | 3 results | happy path |
  1217→| searches products by title | product titled "Organic Cotton Hoodie"; search "cotton" | product found in results | happy path |
  1218→
  1219→---
  1220→
  1221→#### tests/Feature/Products/VariantTest.php
  1222→
  1223→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1224→|-----------|----------------------|----------------|----------|
  1225→| creates variants from option matrix | product with options: Size [S, M, L], Color [Red, Blue]; rebuild matrix | 6 variants created (3x2) | happy path |
  1226→| preserves existing variants when adding an option value | existing: Size [S, M] = 2 variants; add Size L; rebuild matrix | 3 variants, original 2 unchanged (prices preserved) | happy path |
  1227→| archives orphaned variants with order references | variant referenced in order_lines; remove its option value | variant status = archived, not deleted | edge case |
  1228→| deletes orphaned variants without order references | variant not referenced in any order; remove its option value | variant deleted from DB | happy path |
  1229→| auto-creates default variant for products without options | create product with no options | 1 variant with is_default=1 | happy path |
  1230→| validates SKU uniqueness within store | create variant with SKU "TSH-001" in store A; attempt to create another variant with same SKU in store A | validation error | validation |
  1231→| allows duplicate SKU across different stores | SKU "TSH-001" in store A; create variant with "TSH-001" in store B | success | edge case |
  1232→| allows null SKUs | create two variants with null SKU | both created successfully | edge case |
  1233→
  1234→---
  1235→
  1236→#### tests/Feature/Products/InventoryTest.php
  1237→
  1238→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1239→|-----------|----------------------|----------------|----------|
  1240→| creates inventory item when variant is created | create product variant | inventory_item exists with quantity_on_hand=0, quantity_reserved=0 | happy path |
  1241→| checks availability correctly | on_hand=10, reserved=3 | available = 7 | happy path |
  1242→| reserves inventory | on_hand=10, reserved=0; reserve 3 | reserved=3, available=7 | happy path |
  1243→| throws InsufficientInventoryException when reserving more than available with deny policy | on_hand=5, reserved=3, policy=deny; reserve 3 | InsufficientInventoryException thrown | validation |
  1244→| allows overselling with continue policy | on_hand=2, reserved=0, policy=continue; reserve 5 | reserved=5 (no exception) | edge case |
  1245→| releases reserved inventory | on_hand=10, reserved=5; release 3 | reserved=2 | happy path |
  1246→| commits inventory on order completion | on_hand=10, reserved=3; commit 3 | on_hand=7, reserved=0 | happy path |
  1247→| restocks inventory | on_hand=5; restock 10 | on_hand=15 | happy path |
  1248→
  1249→---
  1250→
  1251→#### tests/Feature/Products/CollectionTest.php
  1252→
  1253→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1254→|-----------|----------------------|----------------|----------|
  1255→| creates a collection with a unique handle | create collection titled "Summer Sale" | handle = "summer-sale", saved in DB | happy path |
  1256→| adds products to a collection | create collection, add 3 products | collection_products pivot has 3 rows | happy path |
  1257→| removes products from a collection | collection with 3 products, remove 1 | 2 remain | happy path |
  1258→| reorders products within a collection | 3 products with positions 0,1,2; reorder to 2,0,1 | positions updated correctly | happy path |
  1259→| transitions collection from draft to active | draft collection; transition to active | status = active | happy path |
  1260→| lists collections with product count | collection A with 5 products, collection B with 3 products; list collections | each collection shows correct product count | happy path |
  1261→| scopes collections to current store | store A has 2 collections, store B has 4; current store = A | Collection::count() = 2 | authorization |
  1262→
  1263→---
  1264→
  1265→#### tests/Feature/Products/MediaUploadTest.php
  1266→
  1267→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1268→|-----------|----------------------|----------------|----------|
  1269→| uploads an image for a product | POST Livewire file upload with a test image | product_media row created with status=processing | happy path |
  1270→| processes uploaded image and generates variants | dispatch ProcessMediaUpload job | media status = ready, file exists in storage | happy path |
  1271→| rejects non-image file types | attempt to upload a .txt file | validation error | validation |
  1272→| sets alt text on media | upload image, then update alt_text | alt_text persisted | happy path |
  1273→| reorders media positions | upload 3 images, reorder | position values updated | happy path |
  1274→| deletes media and removes file from storage | upload image, then delete | product_media row removed, file removed from disk | happy path |
  1275→
  1276→---
  1277→
  1278→#### tests/Feature/Cart/CartServiceTest.php
  1279→
  1280→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1281→|-----------|----------------------|----------------|----------|
  1282→| creates a cart for the current store | create cart | cart in DB with store_id, currency, version=1, status=active | happy path |
  1283→| adds a line item to the cart | active product with variant priced at 2500; add to cart qty 2 | cart_line created, unit_price=2500, subtotal=5000, total=5000 | happy path |
  1284→| increments quantity when adding an existing variant | cart already has variant A qty 1; add variant A qty 2 | single line with qty 3, subtotal=7500 | happy path |
  1285→| rejects add when product is not active | product status = draft; attempt to add variant to cart | exception thrown | validation |
  1286→| rejects add when inventory is insufficient and policy is deny | variant on_hand=2, policy=deny; attempt to add qty 5 | InsufficientInventoryException | validation |
  1287→| allows add when inventory is insufficient but policy is continue | variant on_hand=2, policy=continue; add qty 5 | line created with qty 5 | edge case |
  1288→| updates line quantity | cart with variant A qty 2; update to qty 5 | line quantity=5, amounts recalculated | happy path |
  1289→| removes a line when quantity set to zero | update line quantity to 0 | line removed from cart | edge case |
  1290→| removes a specific line item | cart with 2 lines, remove first | 1 line remains | happy path |
  1291→| increments cart version on every mutation | create cart (v1), add line (v2), update qty (v3), remove line (v4) | cart_version = 4 | happy path |
  1292→| returns cart via session for guest users | create cart, store ID in session; get cart for session | same cart returned | happy path |
  1293→| merges guest cart into customer cart on login | guest cart: variant A qty 2; customer cart: variant A qty 1, variant B qty 3; merge | customer cart has A qty 3, B qty 3; guest cart status=abandoned | happy path |
  1294→
  1295→---
  1296→
  1297→#### tests/Feature/Cart/CartApiTest.php
  1298→
  1299→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1300→|-----------|----------------------|----------------|----------|
  1301→| creates a cart via API | POST /api/storefront/v1/carts | 201 response, response contains cart data | happy path |
  1302→| retrieves a cart via API | create cart, then GET /api/storefront/v1/carts/{id} | 200 response, cart data with lines and totals | happy path |
  1303→| 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 |
  1304→| updates line quantity via API | PUT /api/storefront/v1/carts/{id}/lines/{lineId} with quantity | 200 response, quantity updated | happy path |
  1305→| removes a line via API | DELETE /api/storefront/v1/carts/{id}/lines/{lineId} | 200 response, line removed | happy path |
  1306→| returns 404 for nonexistent cart | GET /api/storefront/v1/carts/nonexistent | 404 response | validation |
  1307→| returns 409 on version mismatch | cart at version 3; PUT with expected_version=2 | 409 Conflict with current cart state | validation |
  1308→| respects storefront rate limiting | make 121 requests in 1 minute | 121st returns 429 | validation |
  1309→
  1310→---
  1311→
  1312→#### tests/Feature/Checkout/CheckoutFlowTest.php
  1313→
  1314→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1315→|-----------|----------------------|----------------|----------|
  1316→| creates a checkout from a cart | cart with 2 lines; create checkout | checkout in DB, status=started, linked to cart | happy path |
  1317→| 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 |
  1318→| rejects checkout for empty cart | cart with 0 lines; create checkout | validation error | validation |
  1319→| expires checkout after timeout | checkout with payment_selected status, expires_at in the past; run ExpireAbandonedCheckouts job | status=expired, inventory released | edge case |
  1320→| prevents duplicate orders from same checkout | complete checkout once (creates order); call completeCheckout again | no duplicate order created, same order returned | edge case |
  1321→
  1322→---
  1323→
  1324→#### tests/Feature/Checkout/CheckoutStateTest.php
  1325→
  1326→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1327→|-----------|----------------------|----------------|----------|
  1328→| transitions from started to addressed with valid address | set email and shipping address | status = addressed | happy path |
  1329→| rejects address transition with missing required fields | set address without city | validation error, status remains started | validation |
  1330→| transitions from addressed to shipping_selected | set valid shipping rate | status = shipping_selected | happy path |
  1331→| rejects shipping selection with rate from wrong zone | address in DE, select rate from US-only zone | exception thrown | validation |
  1332→| skips shipping selection when no items require shipping | all variants have requires_shipping=false | transitions directly to shipping_selected with shipping=0 | edge case |
  1333→| transitions from shipping_selected to payment_selected | select payment method | status = payment_selected, expires_at set, inventory reserved | happy path |
  1334→| transitions from payment_selected to completed | simulate successful payment | status = completed, order created | happy path |
  1335→| rejects invalid state transitions | attempt started -> completed | exception thrown | validation |
  1336→| recalculates pricing on address change | set address (DE), note tax calculation; change address to different country | pricing recalculated | happy path |
  1337→
  1338→---
  1339→
  1340→#### tests/Feature/Checkout/PricingIntegrationTest.php
  1341→
  1342→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1343→|-----------|----------------------|----------------|----------|
  1344→| 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 |
  1345→| applies discount code and recalculates | apply 10% discount to 10000 subtotal | discount=1000, discounted subtotal=9000 | happy path |
  1346→| stores pricing snapshot in totals_json | complete pricing calculation | checkout.totals_json contains all fields | happy path |
  1347→| recalculates on shipping method change | select flat rate 499, then switch to weight-based 899 | totals updated with new shipping amount | happy path |
  1348→| handles prices-include-tax correctly | tax settings: prices_include_tax=true, rate=1900; item price 11900 (gross) | tax extracted = 1900, subtotal = 10000 | edge case |
  1349→
  1350→---
  1351→
  1352→#### tests/Feature/Checkout/DiscountTest.php
  1353→
  1354→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1355→|-----------|----------------------|----------------|----------|
  1356→| applies a valid percent discount code at checkout | discount: 10%, code "SAVE10"; apply to checkout with subtotal 5000 | discount = 500 | happy path |
  1357→| applies a valid fixed discount code at checkout | discount: 500 fixed, code "5OFF" | discount = 500 | happy path |
  1358→| removes discount when code is cleared | apply discount, then clear code | discount = 0, totals recalculated | happy path |
  1359→| rejects expired discount at checkout | discount ended yesterday | error returned | validation |
  1360→| increments usage count on order completion | discount usage_count=5; complete checkout with discount | usage_count=6 | happy path |
  1361→| handles free shipping discount at checkout | apply free_shipping discount | shipping = 0 in totals | happy path |
  1362→
  1363→---
  1364→
  1365→#### tests/Feature/Checkout/ShippingTest.php
  1366→
  1367→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1368→|-----------|----------------------|----------------|----------|
  1369→| 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 |
  1370→| returns empty when no zone matches address | store has zone for DE only; address: country FR | empty rates | edge case |
  1371→| calculates flat rate correctly | select flat rate, amount 499 | shipping in totals = 499 | happy path |
  1372→| calculates weight-based rate correctly | cart total weight 750g, rate range 501-2000g = 899 | shipping = 899 | happy path |
  1373→| returns zero shipping when all items are digital | all variants: requires_shipping=false | shipping = 0 | edge case |
  1374→
  1375→---
  1376→
  1377→#### tests/Feature/Checkout/TaxTest.php
  1378→
  1379→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1380→|-----------|----------------------|----------------|----------|
  1381→| calculates exclusive tax correctly at checkout | tax mode: manual, rate: 1900, prices_include_tax: false; discounted subtotal + shipping = 5499 | tax = 1044 (rounded) | happy path |
  1382→| extracts inclusive tax correctly at checkout | tax mode: manual, rate: 1900, prices_include_tax: true | tax extracted from gross amounts | happy path |
  1383→| applies zero tax when no tax settings exist | no tax_settings row for store | tax = 0 | edge case |
  1384→| stores tax lines in totals_json | calculate with rate 1900 | tax_lines array in totals_json has entry with name, rate, amount | happy path |
  1385→
  1386→---
  1387→
  1388→#### tests/Feature/Orders/OrderCreationTest.php
  1389→
  1390→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1391→|-----------|----------------------|----------------|----------|
  1392→| creates an order from a completed checkout | complete checkout flow | order in DB with correct amounts, status=pending | happy path |
  1393→| generates sequential order numbers per store | create 3 orders in store A | order numbers are #1001, #1002, #1003 | happy path |
  1394→| creates order lines with snapshots | order from checkout with 2 items | order_lines have title_snapshot, sku_snapshot | happy path |
  1395→| commits inventory on order creation | variant on_hand=10, reserved=3; create order with qty 3 | on_hand=7, reserved=0 | happy path |
  1396→| marks cart as converted | create order from checkout | cart status = converted | happy path |
  1397→| dispatches OrderCreated event | use Event::fake(); create order | OrderCreated dispatched with order | happy path |
  1398→| preserves order data when product is deleted | create order, then archive the product | order_lines still accessible with snapshots | edge case |
  1399→| links order to customer when authenticated | checkout by logged-in customer | order.customer_id set | happy path |
  1400→| sets email from checkout on the order | guest checkout with email "test@example.com" | order.email = "test@example.com" | happy path |
  1401→
  1402→---
  1403→
  1404→#### tests/Feature/Orders/RefundTest.php
  1405→
  1406→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1407→|-----------|----------------------|----------------|----------|
  1408→| creates a full refund | order with total 5000, payment captured; refund 5000 | refund in DB, financial_status = refunded | happy path |
  1409→| creates a partial refund | order total 5000; refund 2000 | financial_status = partially_refunded | happy path |
  1410→| rejects refund exceeding payment amount | payment amount 5000; attempt to refund 6000 | validation error | validation |
  1411→| restocks inventory when restock flag is true | refund with restock=true, qty 2 | inventory on_hand increased by 2 | happy path |
  1412→| does not restock when restock flag is false | refund with restock=false | inventory unchanged | edge case |
  1413→| only allows admin or owner to process refunds | staff user attempts refund | 403 Forbidden | authorization |
  1414→| records refund reason | refund with reason "Customer requested" | refund.reason = "Customer requested" | happy path |
  1415→
  1416→---
  1417→
  1418→#### tests/Feature/Orders/FulfillmentTest.php
  1419→
  1420→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1421→|-----------|----------------------|----------------|----------|
  1422→| 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 |
  1423→| updates order fulfillment status to partial | fulfill only some lines | order.fulfillment_status = partial | happy path |
  1424→| updates order fulfillment status to fulfilled when all lines done | fulfill all remaining lines | order.fulfillment_status = fulfilled | happy path |
  1425→| adds tracking information | ship fulfillment with tracking_company="DHL", tracking_number="123456" | tracking info saved, shipped_at set | happy path |
  1426→| transitions fulfillment from pending to shipped | ship fulfillment | status = shipped | happy path |
  1427→| transitions fulfillment from shipped to delivered | deliver fulfillment | status = delivered | happy path |
  1428→| prevents fulfilling more than ordered quantity | order line qty 2; attempt to fulfill qty 3 | validation error | validation |
  1429→| fulfillment guard blocks fulfillment when financial_status is pending | order with financial_status=pending (bank transfer) | FulfillmentGuardException thrown | validation |
  1430→| fulfillment guard allows fulfillment when financial_status is paid | order with financial_status=paid | fulfillment created successfully | happy path |
  1431→| fulfillment guard allows fulfillment when financial_status is partially_refunded | order with financial_status=partially_refunded | fulfillment created successfully | edge case |
  1432→| 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 |
  1433→| only allows admin, owner, or staff to create fulfillments | support user attempts to create fulfillment | 403 Forbidden | authorization |
  1434→
  1435→---
  1436→
  1437→#### tests/Feature/Payments/MockPaymentProviderTest.php
  1438→
  1439→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1440→|-----------|----------------------|----------------|----------|
  1441→| charges credit card with success card number | checkout with card 4242424242424242 | PaymentResult success=true, status=captured | happy path |
  1442→| declines credit card with decline card number | checkout with card 4000000000000002 | PaymentResult success=false, errorCode=card_declined | edge case |
  1443→| returns insufficient funds for that card number | checkout with card 4000000000009995 | PaymentResult success=false, errorCode=insufficient_funds | edge case |
  1444→| charges PayPal successfully | checkout with payment_method=paypal | PaymentResult success=true, status=captured | happy path |
  1445→| creates pending payment for bank transfer | checkout with payment_method=bank_transfer | PaymentResult success=true, status=pending | happy path |
  1446→| generates mock reference ID | any charge | provider_payment_id starts with "mock_" | happy path |
  1447→
  1448→---
  1449→
  1450→#### tests/Feature/Payments/PaymentServiceTest.php
  1451→
  1452→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1453→|-----------|----------------------|----------------|----------|
  1454→| processes credit card payment and creates order as paid | checkout with credit card | order created with financial_status=paid, inventory committed | happy path |
  1455→| processes PayPal payment and creates order as paid | checkout with PayPal | order created with financial_status=paid, inventory committed | happy path |
  1456→| 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 |
  1457→| resolves MockPaymentProvider from container | resolve PaymentProvider interface | resolved provider is MockPaymentProvider | happy path |
  1458→| creates a payment record with correct method | charge with credit_card | payment row in DB with method=credit_card, provider=mock | happy path |
  1459→
  1460→---
  1461→
  1462→#### tests/Feature/Payments/BankTransferConfirmationTest.php
  1463→
  1464→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1465→|-----------|----------------------|----------------|----------|
  1466→| admin can confirm bank transfer payment | order with payment_method=bank_transfer, financial_status=pending | financial_status transitions to paid, inventory committed | happy path |
  1467→| cannot confirm payment for non-bank-transfer orders | order with payment_method=credit_card | error thrown | validation |
  1468→| cannot confirm already confirmed payment | order with financial_status=paid | error thrown | validation |
  1469→| 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 |
  1470→| 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 |
  1471→| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |
  1472→
  1473→---
  1474→
  1475→#### tests/Feature/Customers/CustomerAccountTest.php
  1476→
  1477→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1478→|-----------|----------------------|----------------|----------|
  1479→| renders the customer dashboard | acting as customer; GET /account | 200 response, sees customer name | happy path |
  1480→| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
  1481→| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
  1482→| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
  1483→| redirects unauthenticated requests to login | GET /account without auth | redirected to /account/login | authorization |
  1484→| updates customer profile | update name and marketing_opt_in | changes persisted in DB | happy path |
  1485→
  1486→---
  1487→
  1488→#### tests/Feature/Customers/AddressManagementTest.php
  1489→
  1490→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1491→|-----------|----------------------|----------------|----------|
  1492→| lists saved addresses | customer with 2 addresses; GET /account/addresses | sees both addresses | happy path |
  1493→| creates a new address | submit address form | address saved in DB | happy path |
  1494→| updates an existing address | change city on existing address | updated in DB | happy path |
  1495→| deletes an address | delete address | removed from DB | happy path |
  1496→| sets a default address | mark address as default | is_default=1, other addresses is_default=0 | happy path |
  1497→| validates required address fields | submit with missing address1 | validation error | validation |
  1498→| prevents managing another customers addresses | customer A logged in, attempt to edit customer B address | 403 or 404 response | authorization |
  1499→
  1500→---
  1501→
  1502→#### tests/Feature/Admin/DashboardTest.php
  1503→
  1504→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1505→|-----------|----------------------|----------------|----------|
  1506→| renders the admin dashboard | acting as admin; GET /admin | 200 response, sees "Dashboard" | happy path |
  1507→| shows KPI tiles with correct data | store has 5 orders totaling 25000 cents | sees order count 5, total revenue formatted | happy path |
  1508→| restricts dashboard to authenticated admins | unauthenticated GET /admin | redirected to /admin/login | authorization |
  1509→| filters metrics by date range | orders across multiple dates; filter to last 7 days | only matching orders included in KPIs | happy path |
  1510→
  1511→---
  1512→
  1513→#### tests/Feature/Admin/ProductManagementTest.php
  1514→
  1515→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1516→|-----------|----------------------|----------------|----------|
  1517→| lists products with pagination | create 25 products; GET /admin/products | first page shows 15 (or configured per-page), pagination controls visible | happy path |
  1518→| creates a product via admin form | Livewire test: fill title, description, submit | product in DB | happy path |
  1519→| edits a product via admin form | Livewire test: change title, submit | product updated | happy path |
  1520→| bulk archives selected products | select 3 products, trigger bulk archive | all 3 status = archived | happy path |
  1521→| uploads media from the product form | Livewire file upload on product edit | media attached to product | happy path |
  1522→| manages variants from the product form | add options and values via Livewire | variants generated | happy path |
  1523→| 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 |
  1524→| staff can create but not delete products | staff user: create product succeeds; staff user: delete product | fails with 403 on delete | authorization |
  1525→
  1526→---
  1527→
  1528→#### tests/Feature/Admin/OrderManagementTest.php
  1529→
  1530→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1531→|-----------|----------------------|----------------|----------|
  1532→| lists orders with status filter | 3 pending, 2 paid orders; filter by status=paid | 2 results | happy path |
  1533→| shows order detail page | GET /admin/orders/{order} | sees order number, line items, totals | happy path |
  1534→| creates a fulfillment from order detail | Livewire action: select lines, submit fulfillment | fulfillment created | happy path |
  1535→| processes a refund from order detail | Livewire action: enter amount, submit refund | refund created | happy path |
  1536→| restricts order management by role | support user can view orders (read-only); support user cannot update or fulfill | appropriate 403 responses | authorization |
  1537→
  1538→---
  1539→
  1540→#### tests/Feature/Admin/DiscountManagementTest.php
  1541→
  1542→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1543→|-----------|----------------------|----------------|----------|
  1544→| lists discounts | create 3 discounts; GET /admin/discounts | sees all 3 | happy path |
  1545→| creates a percent discount | Livewire: code="SAVE10", type=percent, value=10 | discount in DB | happy path |
  1546→| creates a fixed discount | Livewire: code="5OFF", type=fixed, value=500 | discount in DB | happy path |
  1547→| validates discount code uniqueness within store | create discount with code "SAVE10"; attempt to create another with same code | validation error | validation |
  1548→| edits a discount | change value from 10 to 15 | updated in DB | happy path |
  1549→| disables a discount | set status to disabled | status = disabled | happy path |
  1550→
  1551→---
  1552→
  1553→#### tests/Feature/Admin/SettingsTest.php
  1554→
  1555→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1556→|-----------|----------------------|----------------|----------|
  1557→| renders the settings page | GET /admin/settings | 200 response, sees settings tabs | happy path |
  1558→| updates general store settings | update store name | store_settings updated | happy path |
  1559→| configures shipping zones | create zone with countries and rates | shipping_zone and shipping_rates in DB | happy path |
  1560→| configures tax settings | set mode=manual, rate=1900, prices_include_tax=true | tax_settings updated | happy path |
  1561→| restricts settings to owner and admin roles | staff user GET /admin/settings | 403 response | authorization |
  1562→| manages store domains | add a new domain | store_domain created | happy path |
  1563→
  1564→---
  1565→
  1566→#### tests/Feature/Search/SearchTest.php
  1567→
  1568→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1569→|-----------|----------------------|----------------|----------|
  1570→| returns products matching search query | products: "Blue Cotton T-Shirt", "Red Wool Sweater"; search "cotton" | finds "Blue Cotton T-Shirt" | happy path |
  1571→| 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 |
  1572→| returns empty for no matches | search "xyznonexistent" | empty results | edge case |
  1573→| logs search query for analytics | perform search | search_queries row created | happy path |
  1574→| paginates search results | 25 matching products, 12 per page | page 1 has 12, page 2 has 12, page 3 has 1 | happy path |
  1575→
  1576→---
  1577→
  1578→#### tests/Feature/Search/AutocompleteTest.php
  1579→
  1580→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1581→|-----------|----------------------|----------------|----------|
  1582→| returns suggestions matching prefix | products: "Summer Dress", "Summer Hat", "Winter Coat"; autocomplete prefix "sum" | "Summer Dress" and "Summer Hat" returned | happy path |
  1583→| limits results to configured count | 20 matching products, limit 5 | 5 results returned | happy path |
  1584→| returns empty for very short prefix | autocomplete prefix "a" (below minimum) | empty or all results | edge case |
  1585→
  1586→---
  1587→
  1588→#### tests/Feature/Analytics/EventIngestionTest.php
  1589→
  1590→| Test Name | Setup / Preconditions | Key Assertions | Category |
  1591→|-----------|----------------------|----------------|----------|
  1592→| tracks a page view event | POST analytics event: type=page_view | analytics_events row created | happy path |
  1593→| tracks an add to cart event | POST event: type=add_to_cart, properties include product_id | event stored with properties | happy path |
  1594→| scopes events to current store | track event on store A | event.store_id = store A id | happy path |
  1595→| includes session ID when available | track event with session | session_id stored | happy path |
  1596→| includes customer ID when authenticated | track event as logged-in customer | customer_id stored | happy path |
  1597→
  1598→---
  1599→

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

---


userType: external

gitBranch: claude-subagents

slug: wise-shimmying-pretzel

type: message

role: assistant

service_tier: standard

type: assistant


type: progress

hookEvent: PostToolUse

command: callback


type: user

role: user

content:
/Users/fabianwesner/Herd/shop/app/Providers/FortifyServiceProvider.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/TwoFactor.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/DeleteUserForm.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/TwoFactor/RecoveryCodes.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/Profile.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/Appearance.php
/Users/fabianwesner/Herd/shop/app/Livewire/Actions/Logout.php
/Users/fabianwesner/Herd/shop/app/Http/Controllers/Controller.php
/Users/fabianwesner/Herd/shop/app/Concerns/ProfileValidationRules.php
/Users/fabianwesner/Herd/shop/app/Concerns/PasswordValidationRules.php
/Users/fabianwesner/Herd/shop/app/Actions/Fortify/CreateNewUser.php
/Users/fabianwesner/Herd/shop/app/Actions/Fortify/ResetUserPassword.php
/Users/fabianwesner/Herd/shop/app/Livewire/Settings/Password.php
/Users/fabianwesner/Herd/shop/app/Models/User.php
/Users/fabianwesner/Herd/shop/app/Providers/AppServiceProvider.php


type: thinking

thinking:
Good, I now have a solid understanding of the project scope. The application is a fresh Laravel 12 installation with Fortify and Livewire already set up. I need to build 12 phases of a complete e-commerce platform.

This is a massive implementation task. Let me create a progress tracking file and set up tasks, then start implementing phase by phase using sub-agents.

Let me first understand the remaining spec content, then create the initial progress file and start building.

The approach I'll take:
1. Create the progress.md file
2. Start with Phase 1 (Foundation) - this is CRITICAL, everything depends on it

3. Work through the remaining phases sequentially, leveraging sub-agents for parallel work where feasible
4. Validate each phase with Pest tests and Playwright E2E tests
5. Conduct a final review before completion

I'll start by setting up task tracking and the progress file, then kick off Phase 1 implementation.


type: text

text: Now I have a comprehensive understanding of the project. This is a fresh Laravel 12 app that needs a complete e-commerce platform across 12 phases. Let me create the progress file and start building.


Write

content:
# Implementation Progress

## Phase 1: Foundation (Migrations, Models, Middleware, Auth)
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations
- [ ] Step 1.3: Core Models
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication
- [ ] Step 1.8: Authorization

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

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

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

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

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

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

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

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

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

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

## Phase 12: Full Test Suite
- [ ] All unit tests pass
- [ ] All feature tests pass
- [ ] All Playwright E2E tests pass
- [ ] Code style (Pint) passes
- [ ] Fresh migration + seed works


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

type: create

content:
# Implementation Progress

## Phase 1: Foundation (Migrations, Models, Middleware, Auth)
- [ ] Step 1.1: Environment and Config
- [ ] Step 1.2: Core Migrations
- [ ] Step 1.3: Core Models
- [ ] Step 1.4: Enums
- [ ] Step 1.5: Tenant Resolution Middleware
- [ ] Step 1.6: BelongsToStore Trait and Global Scope
- [ ] Step 1.7: Authentication
- [ ] Step 1.8: Authorization

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

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

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

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

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

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

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

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

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

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

## Phase 12: Full Test Suite
- [ ] All unit tests pass
- [ ] All feature tests pass
- [ ] All Playwright E2E tests pass
- [ ] Code style (Pint) passes
- [ ] Fresh migration + seed works


text: Now let me start implementing Phase 1 - Foundation. This is the most critical phase. I'll first check the current database/migration state and existing config files, then start building.


content:
/Users/fabianwesner/Herd/shop/database/migrations/0001_01_01_000001_create_cache_table.php
/Users/fabianwesner/Herd/shop/database/migrations/0001_01_01_000002_create_jobs_table.php
/Users/fabianwesner/Herd/shop/database/migrations/0001_01_01_000000_create_users_table.php
/Users/fabianwesner/Herd/shop/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php


content:
     1→<?php
     2→
     3→return [
     4→
     5→    /*
     6→    |--------------------------------------------------------------------------
     7→    | Authentication Defaults
     8→    |--------------------------------------------------------------------------
     9→    |
    10→    | This option defines the default authentication "guard" and password
    11→    | reset "broker" for your application. You may change these values
    12→    | as required, but they're a perfect start for most applications.
    13→    |
    14→    */
    15→
    16→    'defaults' => [
    17→        'guard' => env('AUTH_GUARD', 'web'),
    18→        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    19→    ],
    20→
    21→    /*
    22→    |--------------------------------------------------------------------------
    23→    | Authentication Guards
    24→    |--------------------------------------------------------------------------
    25→    |
    26→    | Next, you may define every authentication guard for your application.
    27→    | Of course, a great default configuration has been defined for you
    28→    | which utilizes session storage plus the Eloquent user provider.
    29→    |
    30→    | All authentication guards have a user provider, which defines how the
    31→    | users are actually retrieved out of your database or other storage
    32→    | system used by the application. Typically, Eloquent is utilized.
    33→    |
    34→    | Supported: "session"
    35→    |
    36→    */
    37→
    38→    'guards' => [
    39→        'web' => [
    40→            'driver' => 'session',
    41→            'provider' => 'users',
    42→        ],
    43→    ],
    44→
    45→    /*
    46→    |--------------------------------------------------------------------------
    47→    | User Providers
    48→    |--------------------------------------------------------------------------
    49→    |
    50→    | All authentication guards have a user provider, which defines how the
    51→    | users are actually retrieved out of your database or other storage
    52→    | system used by the application. Typically, Eloquent is utilized.
    53→    |
    54→    | If you have multiple user tables or models you may configure multiple
    55→    | providers to represent the model / table. These providers may then
    56→    | be assigned to any extra authentication guards you have defined.
    57→    |
    58→    | Supported: "database", "eloquent"
    59→    |
    60→    */
    61→
    62→    'providers' => [
    63→        'users' => [
    64→            'driver' => 'eloquent',
    65→            'model' => env('AUTH_MODEL', App\Models\User::class),
    66→        ],
    67→
    68→        // 'users' => [
    69→        //     'driver' => 'database',
    70→        //     'table' => 'users',
    71→        // ],
    72→    ],
    73→
    74→    /*
    75→    |--------------------------------------------------------------------------
    76→    | Resetting Passwords
    77→    |--------------------------------------------------------------------------
    78→    |
    79→    | These configuration options specify the behavior of Laravel's password
    80→    | reset functionality, including the table utilized for token storage
    81→    | and the user provider that is invoked to actually retrieve users.
    82→    |
    83→    | The expiry time is the number of minutes that each reset token will be
    84→    | considered valid. This security feature keeps tokens short-lived so
    85→    | they have less time to be guessed. You may change this as needed.
    86→    |
    87→    | The throttle setting is the number of seconds a user must wait before
    88→    | generating more password reset tokens. This prevents the user from
    89→    | quickly generating a very large amount of password reset tokens.
    90→    |
    91→    */
    92→
    93→    'passwords' => [
    94→        'users' => [
    95→            'provider' => 'users',
    96→            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
    97→            'expire' => 60,
    98→            'throttle' => 60,
    99→        ],
   100→    ],
   101→
   102→    /*
   103→    |--------------------------------------------------------------------------
   104→    | Password Confirmation Timeout
   105→    |--------------------------------------------------------------------------
   106→    |
   107→    | Here you may define the number of seconds before a password confirmation
   108→    | window expires and users are asked to re-enter their password via the
   109→    | confirmation screen. By default, the timeout lasts for three hours.
   110→    |
   111→    */
   112→
   113→    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
   114→
   115→];
   116→

content:
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option defines the default authentication "guard" and password
    | reset "broker" for your application. You may change these values
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | which utilizes session storage plus the Eloquent user provider.
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | If you have multiple user tables or models you may configure multiple
    | providers to represent the model / table. These providers may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | These configuration options specify the behavior of Laravel's password
    | reset functionality, including the table utilized for token storage
    | and the user provider that is invoked to actually retrieve users.
    |
    | The expiry time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    | The throttle setting is the number of seconds a user must wait before
    | generating more password reset tokens. This prevents the user from
    | quickly generating a very large amount of password reset tokens.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the number of seconds before a password confirmation
    | window expires and users are asked to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];


speed: standard


content:
     1→<?php
     2→
     3→use Illuminate\Foundation\Application;
     4→use Illuminate\Foundation\Configuration\Exceptions;
     5→use Illuminate\Foundation\Configuration\Middleware;
     6→
     7→return Application::configure(basePath: dirname(__DIR__))
     8→    ->withRouting(
     9→        web: __DIR__.'/../routes/web.php',
    10→        commands: __DIR__.'/../routes/console.php',
    11→        health: '/up',
    12→    )
    13→    ->withMiddleware(function (Middleware $middleware): void {
    14→        //
    15→    })
    16→    ->withExceptions(function (Exceptions $exceptions): void {
    17→        //
    18→    })->create();
    19→

content:
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();


thinking:
Now I have a clear picture. The app is a fresh Laravel 12 install with Fortify set up. I need to build everything from scratch. Let me start implementing Phase 1 with sub-agents. This is going to be a massive implementation.

Let me take a systematic approach:
1. First, I'll implement Phase 1 (Foundation) - the most critical phase
2. Then proceed through phases 2-12

For Phase 1, I need to:
- Update .env and config files
- Create core migrations (organizations, stores, store_domains, modify users, store_users, store_settings)
- Create core models with factories and seeders

- Set up enums, tenant resolution middleware, and the BelongsToStore trait with global scopes
- Configure authentication for both admin and customer roles, then establish authorization policies

I'll leverage sub-agents to parallelize the foundational work that everything else depends on, starting by examining the existing users migration and .env configuration.


text: Let me check the existing files to understand the starting point.


content:
     1→<?php
     2→
     3→use Illuminate\Database\Migrations\Migration;
     4→use Illuminate\Database\Schema\Blueprint;
     5→use Illuminate\Support\Facades\Schema;
     6→
     7→return new class extends Migration
     8→{
     9→    /**
    10→     * Run the migrations.
    11→     */
    12→    public function up(): void
    13→    {
    14→        Schema::create('users', function (Blueprint $table) {
    15→            $table->id();
    16→            $table->string('name');
    17→            $table->string('email')->unique();
    18→            $table->timestamp('email_verified_at')->nullable();
    19→            $table->string('password');
    20→            $table->rememberToken();
    21→            $table->timestamps();
    22→        });
    23→
    24→        Schema::create('password_reset_tokens', function (Blueprint $table) {
    25→            $table->string('email')->primary();
    26→            $table->string('token');
    27→            $table->timestamp('created_at')->nullable();
    28→        });
    29→
    30→        Schema::create('sessions', function (Blueprint $table) {
    31→            $table->string('id')->primary();
    32→            $table->foreignId('user_id')->nullable()->index();
    33→            $table->string('ip_address', 45)->nullable();
    34→            $table->text('user_agent')->nullable();
    35→            $table->longText('payload');
    36→            $table->integer('last_activity')->index();
    37→        });
    38→    }
    39→
    40→    /**
    41→     * Reverse the migrations.
    42→     */
    43→    public function down(): void
    44→    {
    45→        Schema::dropIfExists('users');
    46→        Schema::dropIfExists('password_reset_tokens');
    47→        Schema::dropIfExists('sessions');
    48→    }
    49→};
    50→

content:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};


content:
     1→APP_NAME=Laravel
     2→APP_ENV=local
     3→APP_KEY=base64:nW7zTogbucXWMRU2+3CxeYXYni4FTNdm8nIUeG7tOzQ=
     4→APP_DEBUG=true
     5→APP_URL=http://shop.test
     6→
     7→APP_LOCALE=en
     8→APP_FALLBACK_LOCALE=en
     9→APP_FAKER_LOCALE=en_US
    10→
    11→APP_MAINTENANCE_DRIVER=file
    12→# APP_MAINTENANCE_STORE=database
    13→
    14→# PHP_CLI_SERVER_WORKERS=4
    15→
    16→BCRYPT_ROUNDS=12
    17→
    18→LOG_CHANNEL=stack
    19→LOG_STACK=single
    20→LOG_DEPRECATIONS_CHANNEL=null
    21→LOG_LEVEL=debug
    22→
    23→DB_CONNECTION=sqlite
    24→# DB_HOST=127.0.0.1
    25→# DB_PORT=3306
    26→# DB_DATABASE=laravel
    27→# DB_USERNAME=root
    28→# DB_PASSWORD=
    29→
    30→SESSION_DRIVER=file
    31→SESSION_LIFETIME=120
    32→SESSION_ENCRYPT=false
    33→SESSION_PATH=/
    34→SESSION_DOMAIN=null
    35→
    36→BROADCAST_CONNECTION=log
    37→FILESYSTEM_DISK=local
    38→QUEUE_CONNECTION=sync
    39→
    40→CACHE_STORE=file
    41→# CACHE_PREFIX=
    42→
    43→MEMCACHED_HOST=127.0.0.1
    44→
    45→REDIS_CLIENT=phpredis
    46→REDIS_HOST=127.0.0.1
    47→REDIS_PASSWORD=null
    48→REDIS_PORT=6379
    49→
    50→MAIL_MAILER=log
    51→MAIL_SCHEME=null
    52→MAIL_HOST=127.0.0.1
    53→MAIL_PORT=2525
    54→MAIL_USERNAME=null
    55→MAIL_PASSWORD=null
    56→MAIL_FROM_ADDRESS="hello@example.com"
    57→MAIL_FROM_NAME="${APP_NAME}"
    58→
    59→AWS_ACCESS_KEY_ID=
    60→AWS_SECRET_ACCESS_KEY=
    61→AWS_DEFAULT_REGION=us-east-1
    62→AWS_BUCKET=
    63→AWS_USE_PATH_STYLE_ENDPOINT=false
    64→
    65→VITE_APP_NAME="${APP_NAME}"
    66→FORGE_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZTJlODczZTViNzQxOWIwMjk0MDA2MjI4Y2RlNzliMmM5ZDNhNjEzYWJkM2Q3NmMxNWYzZTI3OGE1NTRiZDgwNjk3ZDQ3ZWU1YWM1MmViNmMiLCJpYXQiOjE3NjkxODU2NTUuMDc1NjE0LCJuYmYiOjE3NjkxODU2NTUuMDc1NjE3LCJleHAiOjIwODQ3MTg0NTUuMDcyNzExLCJzdWIiOiIyNDE3NDgiLCJzY29wZXMiOlsic2VydmVyOnZpZXciLCJzZXJ2ZXI6Y3JlYXRlIiwic2VydmVyOmRlbGV0ZSIsInNlcnZlcjphcmNoaXZlIiwic2VydmVyOnRyYW5zZmVyIiwic2VydmVyOm1hbmFnZS1tZXRhIiwic2VydmVyOm1hbmFnZS1wYWNrYWdlcyIsInNlcnZlcjptYW5hZ2UtcGhwIiwic2VydmVyOm1hbmFnZS1sb2dzIiwic2VydmVyOm1hbmFnZS1uZXR3b3JrIiwic2VydmVyOm1hbmFnZS1uZ2lueC10ZW1wbGF0ZXMiLCJzZXJ2ZXI6bWFuYWdlLXNlcnZpY2VzIiwic2VydmVyOm1hbmFnZS1wYXNzd29yZCIsInNlcnZlcjpjcmVhdGUta2V5cyIsInNlcnZlcjpkZWxldGUta2V5cyIsInNlcnZlcjpjcmVhdGUtbW9uaXRvcnMiLCJzZXJ2ZXI6ZGVsZXRlLW1vbml0b3JzIiwic2VydmVyOmNyZWF0ZS1kYXRhYmFzZXMiLCJzZXJ2ZXI6ZGVsZXRlLWRhdGFiYXNlcyIsInNlcnZlcjpjcmVhdGUtYmFja3VwcyIsInNlcnZlcjpkZWxldGUtYmFja3VwcyIsInNlcnZlcjpjcmVhdGUtZGFlbW9ucyIsInNlcnZlcjpkZWxldGUtZGFlbW9ucyIsInNlcnZlcjpjcmVhdGUtc2NoZWR1bGVycyIsInNlcnZlcjpkZWxldGUtc2NoZWR1bGVycyIsInNlcnZlcjp3ZWItdGVybWluYWwiLCJzaXRlOmNyZWF0ZSIsInNpdGU6ZGVsZXRlIiwic2l0ZTptZXRhIiwic2l0ZTptYW5hZ2UtY29tbWFuZHMiLCJzaXRlOm1hbmFnZS1kZXBsb3lzIiwic2l0ZTptYW5hZ2UtbmdpbngiLCJzaXRlOm1hbmFnZS1wcm9qZWN0Iiwic2l0ZTptYW5hZ2UtZW52aXJvbm1lbnQiLCJzaXRlOm1hbmFnZS1ub3RpZmljYXRpb25zIiwic2l0ZTptYW5hZ2UtcXVldWVzIiwic2l0ZTptYW5hZ2UtcmVkaXJlY3RzIiwic2l0ZTptYW5hZ2Utc2VjdXJpdHkiLCJzaXRlOm1hbmFnZS1zc2wiLCJzaXRlOm1hbmFnZS1pbnRlZ3JhdGlvbnMiLCJzaXRlOm1hbmFnZS1oZWFydGJlYXRzIiwic3RvcmFnZTptYW5hZ2UiXX0.jnUjxC3lIQJLCTJ1AbgVDQ-JhExT7gQASOPMdtCWAW8HQbFEz4uvOERS1xGvwCFiUe0dJPt1O1G_Npi9SdjJaKiFJE-MrUoyKTkEOO41Ay6hftxx73p3QlyGfXZkx0IREaX8bgn6JwPFy5CLiOXhTzvBLj3JL0QVR2gUxPtGBgatO5KLQUyA8UCpA_mikRP2itUTshf0t2xF-fwKK6PK2iOIsG4TCaMpt3qPE_CJWataxQKOKFFa1qsek_LLwTwePfnEboys27wGht0G4-sMRTs_QzsGwwhrhpRm3f_16c7uH_mCNDhMLccJrl_N82yaJVPkgcpIag8hhx2sOtjNA6bl8ghyKA4-tdM1CwmFXeK-Lexd1pjyza9J8lG3cezK5wTlcNfrkTkhUNdE51h0IqCAVSnGxlfJIlzBJX9xOZl_NNdl4RItdoxUccfbbsZ7V-OjWoFEnZG2PTeupgnOGm10eEqd4nZkXD3TX_ecqX54pgmEakir3p_as7KVN1EygJlGGrv9FsZrdnyseah127Zz1EG1Mrh8LMVZv9SxQtvsE4Qw77N_rNmNznSgBVRgp7CBkglkolWijB8cGN2ktvVOGSeOJp77Oe1wfnH5E1VDochsDUW2R7ouFx6bJwS6_SV__wLOSjLYRmqHu9rscTKFPAo3A-ZsgJQGyfK7TIw"
    67→
    68→DB_DATABASE=database/shop.sqlite
    69→

content:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:nW7zTogbucXWMRU2+3CxeYXYni4FTNdm8nIUeG7tOzQ=
APP_DEBUG=true
APP_URL=http://shop.test

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database

# PHP_CLI_SERVER_WORKERS=4

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

CACHE_STORE=file
# CACHE_PREFIX=

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"
FORGE_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZTJlODczZTViNzQxOWIwMjk0MDA2MjI4Y2RlNzliMmM5ZDNhNjEzYWJkM2Q3NmMxNWYzZTI3OGE1NTRiZDgwNjk3ZDQ3ZWU1YWM1MmViNmMiLCJpYXQiOjE3NjkxODU2NTUuMDc1NjE0LCJuYmYiOjE3NjkxODU2NTUuMDc1NjE3LCJleHAiOjIwODQ3MTg0NTUuMDcyNzExLCJzdWIiOiIyNDE3NDgiLCJzY29wZXMiOlsic2VydmVyOnZpZXciLCJzZXJ2ZXI6Y3JlYXRlIiwic2VydmVyOmRlbGV0ZSIsInNlcnZlcjphcmNoaXZlIiwic2VydmVyOnRyYW5zZmVyIiwic2VydmVyOm1hbmFnZS1tZXRhIiwic2VydmVyOm1hbmFnZS1wYWNrYWdlcyIsInNlcnZlcjptYW5hZ2UtcGhwIiwic2VydmVyOm1hbmFnZS1sb2dzIiwic2VydmVyOm1hbmFnZS1uZXR3b3JrIiwic2VydmVyOm1hbmFnZS1uZ2lueC10ZW1wbGF0ZXMiLCJzZXJ2ZXI6bWFuYWdlLXNlcnZpY2VzIiwic2VydmVyOm1hbmFnZS1wYXNzd29yZCIsInNlcnZlcjpjcmVhdGUta2V5cyIsInNlcnZlcjpkZWxldGUta2V5cyIsInNlcnZlcjpjcmVhdGUtbW9uaXRvcnMiLCJzZXJ2ZXI6ZGVsZXRlLW1vbml0b3JzIiwic2VydmVyOmNyZWF0ZS1kYXRhYmFzZXMiLCJzZXJ2ZXI6ZGVsZXRlLWRhdGFiYXNlcyIsInNlcnZlcjpjcmVhdGUtYmFja3VwcyIsInNlcnZlcjpkZWxldGUtYmFja3VwcyIsInNlcnZlcjpjcmVhdGUtZGFlbW9ucyIsInNlcnZlcjpkZWxldGUtZGFlbW9ucyIsInNlcnZlcjpjcmVhdGUtc2NoZWR1bGVycyIsInNlcnZlcjpkZWxldGUtc2NoZWR1bGVycyIsInNlcnZlcjp3ZWItdGVybWluYWwiLCJzaXRlOmNyZWF0ZSIsInNpdGU6ZGVsZXRlIiwic2l0ZTptZXRhIiwic2l0ZTptYW5hZ2UtY29tbWFuZHMiLCJzaXRlOm1hbmFnZS1kZXBsb3lzIiwic2l0ZTptYW5hZ2UtbmdpbngiLCJzaXRlOm1hbmFnZS1wcm9qZWN0Iiwic2l0ZTptYW5hZ2UtZW52aXJvbm1lbnQiLCJzaXRlOm1hbmFnZS1ub3RpZmljYXRpb25zIiwic2l0ZTptYW5hZ2UtcXVldWVzIiwic2l0ZTptYW5hZ2UtcmVkaXJlY3RzIiwic2l0ZTptYW5hZ2Utc2VjdXJpdHkiLCJzaXRlOm1hbmFnZS1zc2wiLCJzaXRlOm1hbmFnZS1pbnRlZ3JhdGlvbnMiLCJzaXRlOm1hbmFnZS1oZWFydGJlYXRzIiwic3RvcmFnZTptYW5hZ2UiXX0.jnUjxC3lIQJLCTJ1AbgVDQ-JhExT7gQASOPMdtCWAW8HQbFEz4uvOERS1xGvwCFiUe0dJPt1O1G_Npi9SdjJaKiFJE-MrUoyKTkEOO41Ay6hftxx73p3QlyGfXZkx0IREaX8bgn6JwPFy5CLiOXhTzvBLj3JL0QVR2gUxPtGBgatO5KLQUyA8UCpA_mikRP2itUTshf0t2xF-fwKK6PK2iOIsG4TCaMpt3qPE_CJWataxQKOKFFa1qsek_LLwTwePfnEboys27wGht0G4-sMRTs_QzsGwwhrhpRm3f_16c7uH_mCNDhMLccJrl_N82yaJVPkgcpIag8hhx2sOtjNA6bl8ghyKA4-tdM1CwmFXeK-Lexd1pjyza9J8lG3cezK5wTlcNfrkTkhUNdE51h0IqCAVSnGxlfJIlzBJX9xOZl_NNdl4RItdoxUccfbbsZ7V-OjWoFEnZG2PTeupgnOGm10eEqd4nZkXD3TX_ecqX54pgmEakir3p_as7KVN1EygJlGGrv9FsZrdnyseah127Zz1EG1Mrh8LMVZv9SxQtvsE4Qw77N_rNmNznSgBVRgp7CBkglkolWijB8cGN2ktvVOGSeOJp77Oe1wfnH5E1VDochsDUW2R7ouFx6bJwS6_SV__wLOSjLYRmqHu9rscTKFPAo3A-ZsgJQGyfK7TIw"

DB_DATABASE=database/shop.sqlite


content:
     1→<?php
     2→
     3→use Illuminate\Support\Facades\Route;
     4→
     5→Route::get('/', function () {
     6→    return view('welcome');
     7→})->name('home');
     8→
     9→Route::view('dashboard', 'dashboard')
    10→    ->middleware(['auth', 'verified'])
    11→    ->name('dashboard');
    12→
    13→require __DIR__.'/settings.php';
    14→

content:
<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
})->name('home');

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';


content:
     1→<?php
     2→
     3→namespace App\Models;
     4→
     5→// use Illuminate\Contracts\Auth\MustVerifyEmail;
     6→use Illuminate\Database\Eloquent\Factories\HasFactory;
     7→use Illuminate\Foundation\Auth\User as Authenticatable;
     8→use Illuminate\Notifications\Notifiable;
     9→use Illuminate\Support\Str;
    10→use Laravel\Fortify\TwoFactorAuthenticatable;
    11→
    12→class User extends Authenticatable
    13→{
    14→    /** @use HasFactory<\Database\Factories\UserFactory> */
    15→    use HasFactory, Notifiable, TwoFactorAuthenticatable;
    16→
    17→    /**
    18→     * The attributes that are mass assignable.
    19→     *
    20→     * @var list<string>
    21→     */
    22→    protected $fillable = [
    23→        'name',
    24→        'email',
    25→        'password',
    26→    ];
    27→
    28→    /**
    29→     * The attributes that should be hidden for serialization.
    30→     *
    31→     * @var list<string>
    32→     */
    33→    protected $hidden = [
    34→        'password',
    35→        'two_factor_secret',
    36→        'two_factor_recovery_codes',
    37→        'remember_token',
    38→    ];
    39→
    40→    /**
    41→     * Get the attributes that should be cast.
    42→     *
    43→     * @return array<string, string>
    44→     */
    45→    protected function casts(): array
    46→    {
    47→        return [
    48→            'email_verified_at' => 'datetime',
    49→            'password' => 'hashed',
    50→        ];
    51→    }
    52→
    53→    /**
    54→     * Get the user's initials
    55→     */
    56→    public function initials(): string
    57→    {
    58→        return Str::of($this->name)
    59→            ->explode(' ')
    60→            ->take(2)
    61→            ->map(fn ($word) => Str::substr($word, 0, 1))
    62→            ->implode('');
    63→    }
    64→}
    65→

content:
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    /**
     * Get the user's initials
     */
    public function initials(): string
    {
        return Str::of($this->name)
            ->explode(' ')
            ->take(2)
            ->map(fn ($word) => Str::substr($word, 0, 1))
            ->implode('');
    }
}