Update DemoSeeder + write tests

Theme/ThemeSettings/Page/NavigationMenu seed data; HomepageTest, ProductShowTest, PageShowTest, NavigationTest

Create NavigationService + ThemeSettingsService

Cache-backed navigation tree + singleton theme settings loader

Phase 6: Customer Accounts

Customer auth (guard + provider from Phase 1). Livewire components: Storefront\\Account\\Auth\\Login, Register, Dashboard, Orders\\Index, Orders\\Show, Addresses\\Index. Routes /account, /account/login, /account/register, /account/orders, /account/addresses. Logout action. Tests CustomerAccountTest, AddressManagementTest.

Phase 7: Admin Panel

Admin layout (Sidebar/TopBar/Breadcrumbs/Flux). Livewire components: Admin\\Dashboard (KPIs, chart, recent orders), Admin\\Products\\Index + Form (shared form for create/edit), Admin\\Orders\\Index + Show (+ fulfillment/refund modals), Admin\\Collections\\Index + Form, Admin\\Customers\\Index + Show, Admin\\Discounts\\Index + Form, Admin\\Settings\\Index (+ Shipping + Taxes sub-tabs), Admin\\Themes\\Index (+ Editor), Admin\\Pages\\Index + Form, Admin\\Navigation\\Index, Admin\\Analytics\\Index, Admin\\Search\\Settings, Admin\\Apps\\Index + Show, Admin\\Developers\\Index. Toast notifications via Livewire events. Tests DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest.

Create storefront Blade components

Build Orders Index/Show Livewire

Orders list with pagination + order detail view with lines, addresses, fulfillments

Phase 10: Apps and Webhooks

Migrations: apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries. WebhookService (dispatch/sign/verify). DeliverWebhook job (retry backoff 60/300/1800/7200/43200s, HMAC-SHA256, circuit breaker after 5 failures). Tests WebhookDeliveryTest, WebhookSignatureTest.

Build Products admin (Index + Form)

List with search/status/pagination/bulk actions; Form for create/edit with title, description, status, vendor, product_type, tags, variants, media upload.

Build Analytics/Search/Apps/Developers admin

Analytics Index (class_exists guarded), Search Settings with reindex, Apps Index+Show stubs, Developers PAT + webhooks.

Wire routes + sidebar + write feature tests

Add all routes in routes/admin.php with named routes. Enhance sidebar. Write tests: DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest. Pint + commit.

Build Orders admin (Index + Show)

Index with filters/search/date range; Show with timeline, line items, payment, fulfillment modal, refund modal, confirm-bank-transfer, cancel.

Phase 1: Foundation

Install Sanctum; set up auth guards (web/customer/sanctum); create core migrations (organizations, stores, store_domains, store_users, store_settings, modify users); create Organization/Store/StoreDomain/StoreUser/StoreSettings models + factories + seeders; add core enums (StoreStatus, StoreUserRole, StoreDomainType); create ResolveStore middleware (resolves storefront by Host header, admin by session); register storefront + admin middleware groups in bootstrap/app.php; create App\Models\Concerns\BelongsToStore trait + App\Models\Scopes\StoreScope; scaffold admin and customer auth (Livewire login/logout, customer guard, CustomerUserProvider, rate limiting); create authorization policies (Product/Order/Collection/Discount/Customer/Store/Page/Theme/Fulfillment/Refund) with role helper; admin routes under /admin/*; storefront routes on storefront host; 404 for unknown host, 503 for suspended store. Run migrations and seed. Write Pest tests for Tenancy (TenantResolutionTest, StoreIsolationTest) + Auth (AdminAuthTest, CustomerAuthTest, SanctumTokenTest). All tests must pass.

Phase 11: Polish

Accessibility audit (skip links, ARIA labels, focus mgmt in modals). Responsive breakpoints (sm/md/lg/xl). Dark mode (dark: prefix). Error pages 404/503 themed. Structured JSON logging channel. Seed data (demo store: storefront + admin + customers + products + orders).

Phase 2: Catalog

Build product catalog: migrations for products/product_options/product_option_values/product_variants/variant_option_values/inventory_items/collections/collection_products/product_media. Models with relationships + factories + seeders. Enums: ProductStatus, VariantStatus, CollectionStatus, MediaType, MediaStatus, InventoryPolicy. Services: ProductService (CRUD, status transitions), VariantMatrixService (rebuildMatrix), InventoryService (check/reserve/release/commit/restock), HandleGenerator (unique slugs per store), ProcessMediaUpload job. All services use DB transactions. Write unit tests HandleGeneratorTest, feature tests ProductCrudTest, VariantTest, InventoryTest, CollectionTest, MediaUploadTest. All pass.

Build Collections/Customers/Discounts admin

Collections Index+Form with product picker/reorder. Customers Index+Show. Discounts Index+Form.

Phase 12: Full Test Suite + Playwright Review

Run php artisan test, fix any failures. Run vendor/bin/pint. Fresh migrate --seed. Write + run Pest browser tests under tests/Browser/ as described in specs/08-PLAYWRIGHT-E2E-PLAN.md for full storefront + admin smoke. Playwright MCP: live review on http://shop.test demonstrating every feature (customer side: home, product, cart, checkout, order confirmation, account login/orders; admin side: login, dashboard, products CRUD, orders, discounts, settings). Fix any bugs encountered; restart the review until no bugs remain.

Write CustomerAccountTest + AddressManagementTest

Covers login/register/dashboard redirects, orders list scoped to customer, address CRUD with default semantics

Enhance Dashboard with real metrics

Add date range filter, fetch orders in range, compute sales/orders/AOV/conversion, show recent orders table.

Create Phase 3 migrations

themes, theme_files, theme_settings, pages, navigation_menus, navigation_items with proper indexes and FKs

Build Settings + Themes + Pages + Navigation admin

Settings (general/domains/shipping/taxes/notifications), Themes Index+Editor, Pages Index+Form, Navigation Index.

Phase 3: Themes and Storefront Layout

Migrations: themes, theme_files, theme_settings, pages, navigation_menus, navigation_items. Models + factories + seeders. Enums: ThemeStatus, PageStatus, NavigationItemType. Build storefront Blade layout (resources/views/storefront/): layouts/app.blade.php with header/announcement/main/footer/cart drawer (dark-mode ready). Pages: home, collection, product, cart, search, page. Account pages: login, register, dashboard, orders index/show, addresses. Error pages: 404, 503. Storefront components: product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination. Storefront Livewire components: Home, Collections\\Index, Collections\\Show, Products\\Show, Cart\\Show, CartDrawer, Search\\Index, Search\\Modal, Pages\\Show. NavigationService with buildTree and resolveUrl. ThemeSettings singleton. Tests to confirm pages render.

Phase 8: Search

Migrations: search_settings, search_queries, FTS5 virtual products_fts table. SearchService (search/autocomplete/syncProduct/removeProduct). ProductObserver hooking create/update/delete. Storefront Search\\Modal + Search\\Index UI. Tests SearchTest, AutocompleteTest.

Build Addresses Index + add/edit/delete

Address book with create/edit modal + set default + delete confirm

Create storefront Livewire components

Collections Index/Show, Products Show, Cart Show placeholder, Search Index/Modal, Pages Show; enhance Home

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

Migrations: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models + factories + seeders. Enums: CartStatus, CheckoutStatus, DiscountType, DiscountValueType, DiscountStatus, ShippingRateType, TaxMode. Services: CartService (create/addLine/updateLine/removeLine, session binding, mergeOnLogin, version increments), DiscountService (validate/calculate), ShippingCalculator (getAvailableRates/calculate), TaxCalculator (calculate/extractInclusive/addExclusive), PricingEngine (full pipeline), CheckoutService state machine, ExpireAbandonedCheckouts + CleanupAbandonedCarts jobs. ValueObjects PricingResult + TaxLine. Storefront UI: CartDrawer, Cart\\Show, Checkout\\Show (stepper), Checkout\\Confirmation. Unit tests PricingEngineTest, DiscountCalculatorTest, TaxCalculatorTest, ShippingCalculatorTest, CartVersionTest. Feature tests CartServiceTest, CartApiTest, CheckoutFlowTest, CheckoutStateTest, PricingIntegrationTest, DiscountTest, ShippingTest, TaxTest. All pass.

Create Phase 3 enums

Create Phase 3 models + factories

Theme, ThemeFile, ThemeSettings, Page, NavigationMenu, NavigationItem with relations

Phase 5: Payments, Orders, Fulfillment

Migrations: customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Models + factories + seeders. Enums: OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus. Contract PaymentProvider + MockPaymentProvider (magic card numbers for credit_card, PayPal instant, bank_transfer pending). OrderService (createFromCheckout with snapshots, generateOrderNumber sequential per store, cancel). RefundService. FulfillmentService (guard: only if paid or partially_refunded). CancelUnpaidBankTransferOrders job. Events: OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded. Tests MockPaymentProviderTest, PaymentServiceTest, BankTransferConfirmationTest, OrderCreationTest, RefundTest, FulfillmentTest.

Wire routes + error views

Update storefront.php routes and add 404/503 error views

Wire account routes + enhance dashboard

Add orders + addresses routes; show recent orders on dashboard

Phase 9: Analytics

Migrations: analytics_events, analytics_daily. AnalyticsService (track/getDailyMetrics). AggregateAnalytics daily job. Tests EventIngestionTest, AggregationTest.


Updated task #11 status


Updated task #12 owner, status


Starting live Playwright review of the storefront and admin.


### Ran Playwright code
```js
await page.goto('http://shop.test/');
```
### Page
- Page URL: http://shop.test/
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-39-15-903Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-39-15-591Z.log#L1-L2


Your mission is to implement an entire shop system based on the specifications im specs/*. Distribute work to sub-agents. Keep the context of the main agent clean. You have to build in one go without…


### Page
- Page URL: http://shop.test/
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - link "Skip to content" [ref=e2] [cursor=pointer]:
    - /url: "#main-content"
  - generic [ref=e3]: Free shipping on orders over 50
  - banner [ref=e4]:
    - generic [ref=e5]:
      - link "Acme Fashion" [ref=e6] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e7]:
        - link "Collections" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/collections
        - link "Search" [ref=e9] [cursor=pointer]:
          - /url: http://shop.test/search
        - link "Account" [ref=e10] [cursor=pointer]:
          - /url: http://shop.test/account
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
  - main [ref=e12]:
    - generic [ref=e13]:
      - generic [ref=e14]:
        - generic [ref=e15]: Elevated essentials
        - paragraph [ref=e16]: Timeless pieces for modern wardrobes.
        - link "Shop the collection" [ref=e18] [cursor=pointer]:
          - /url: /collections
      - generic [ref=e19]:
        - generic [ref=e20]: Featured collections
        - link "Featured" [ref=e22] [cursor=pointer]:
          - /url: http://shop.test/collections/featured
          - generic [ref=e23]: Featured
      - generic [ref=e24]:
        - generic [ref=e25]: Featured products
        - generic [ref=e26]:
          - link "Organic Cotton T-Shirt" [ref=e28] [cursor=pointer]:
            - /url: http://shop.test/products/organic-cotton-t-shirt
            - img [ref=e31]
            - generic [ref=e34]: Organic Cotton T-Shirt
          - link "Classic Pullover Hoodie" [ref=e36] [cursor=pointer]:
            - /url: http://shop.test/products/classic-pullover-hoodie
            - img [ref=e39]
            - generic [ref=e42]: Classic Pullover Hoodie
  - contentinfo [ref=e43]:
    - generic [ref=e44]: © 2026 Acme Fashion. All rights reserved.
  - generic:
    - generic:
      - dialog "Shopping cart":
        - banner:
          - generic: Your Cart (0)
          - button "Close cart":
            - generic:
              - img
            - img
        - generic:
          - generic:
            - img
            - paragraph: Your cart is empty
            - button "Continue shopping":
              - generic:
                - img
              - generic: Continue shopping
```


### Result
Total messages: 4 (Errors: 1, Warnings: 0)
Returning 2 messages for level "info"

[LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/:49
[ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://shop.test/favicon.ico:0


Home loads with featured collection + products. Clicking product to view detail.


### Ran Playwright code
```js
await page.getByRole('link', { name: 'Organic Cotton T-Shirt' }).click();
```
### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-39-24-839Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-39-15-591Z.log#L3-L4


### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - link "Skip to content" [ref=e2] [cursor=pointer]:
    - /url: "#main-content"
  - generic [ref=e3]: Free shipping on orders over 50
  - banner [ref=e4]:
    - generic [ref=e5]:
      - link "Acme Fashion" [ref=e6] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e7]:
        - link "Collections" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/collections
        - link "Search" [ref=e9] [cursor=pointer]:
          - /url: http://shop.test/search
        - link "Account" [ref=e10] [cursor=pointer]:
          - /url: http://shop.test/account
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
  - main [ref=e12]:
    - generic [ref=e13]:
      - navigation "Breadcrumb" [ref=e14]:
        - list [ref=e15]:
          - listitem [ref=e16]:
            - link "Home" [ref=e17] [cursor=pointer]:
              - /url: http://shop.test
            - img [ref=e18]
          - listitem [ref=e20]:
            - generic [ref=e21]: Products
            - img [ref=e22]
          - listitem [ref=e24]:
            - generic [ref=e25]: Organic Cotton T-Shirt
      - generic [ref=e26]:
        - img "Organic Cotton T-Shirt front" [ref=e29]
        - generic [ref=e30]:
          - generic [ref=e31]: Organic Cotton T-Shirt
          - generic [ref=e33]: 25.00 EUR
          - generic [ref=e34]:
            - generic [ref=e35]: Variant
            - combobox "Variant" [ref=e36]:
              - option "TSH-S-BLA" [selected]
              - option "TSH-S-WHI"
              - option "TSH-M-BLA"
              - option "TSH-M-WHI"
              - option "TSH-L-BLA"
              - option "TSH-L-WHI"
          - generic [ref=e37]:
            - generic [ref=e38]: Quantity
            - spinbutton "Quantity" [ref=e39]: "1"
          - button "Add to cart" [ref=e40]:
            - img [ref=e42]
            - generic [ref=e45]: Add to cart
          - paragraph [ref=e47]: Soft, breathable, sustainably sourced.
          - generic [ref=e48]:
            - generic [ref=e49]: summer
            - generic [ref=e50]: bestseller
  - contentinfo [ref=e51]:
    - generic [ref=e52]: © 2026 Acme Fashion. All rights reserved.
  - generic:
    - generic:
      - dialog "Shopping cart":
        - banner:
          - generic: Your Cart (0)
          - button "Close cart":
            - generic:
              - img
            - img
        - generic:
          - generic:
            - img
            - paragraph: Your cart is empty
            - button "Continue shopping":
              - generic:
                - img
              - generic: Continue shopping
```


### Ran Playwright code
```js
await page.getByTestId('add-to-cart').click();
```
### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-39-31-590Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-39-15-591Z.log#L5


### Result
Total messages: 5 (Errors: 2, Warnings: 0)
Returning 3 messages for level "info"

[LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test/_boost/browser-logs @ http://shop.test/products/organic-cotton-t-shirt:49
[ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ http://shop.test/storage/products/tshirt-front.jpg:0
[ERROR] Failed to load resource: the server responded with a status of 403 (Forbidden) @ http://shop.test/storage/products/tshirt-front.jpg:0


### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
```yaml
- generic [ref=e1]:
  - link "Skip to content" [ref=e2] [cursor=pointer]:
    - /url: "#main-content"
  - generic [ref=e3]: Free shipping on orders over 50
  - banner [ref=e4]:
    - generic [ref=e5]:
      - link "Acme Fashion" [ref=e6] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e7]:
        - link "Collections" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/collections
        - link "Search" [ref=e9] [cursor=pointer]:
          - /url: http://shop.test/search
        - link "Account" [ref=e10] [cursor=pointer]:
          - /url: http://shop.test/account
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
  - main [ref=e12]:
    - generic [ref=e13]:
      - navigation "Breadcrumb" [ref=e14]:
        - list [ref=e15]:
          - listitem [ref=e16]:
            - link "Home" [ref=e17] [cursor=pointer]:
              - /url: http://shop.test
            - img [ref=e18]
          - listitem [ref=e20]:
            - generic [ref=e21]: Products
            - img [ref=e22]
          - listitem [ref=e24]:
            - generic [ref=e25]: Organic Cotton T-Shirt
      - generic [ref=e26]:
        - img "Organic Cotton T-Shirt front" [ref=e29]
        - generic [ref=e30]:
          - generic [ref=e31]: Organic Cotton T-Shirt
          - generic [ref=e33]: 25.00 EUR
          - generic [ref=e34]:
            - generic [ref=e35]: Variant
            - combobox "Variant" [ref=e36]:
              - option "TSH-S-BLA" [selected]
              - option "TSH-S-WHI"
              - option "TSH-M-BLA"
              - option "TSH-M-WHI"
              - option "TSH-L-BLA"
              - option "TSH-L-WHI"
          - generic [ref=e37]:
            - generic [ref=e38]: Quantity
            - spinbutton "Quantity" [ref=e39]: "1"
          - button "Add to cart" [active] [ref=e40]:
            - img [ref=e42]
            - generic [ref=e45]: Add to cart
          - paragraph [ref=e47]: Soft, breathable, sustainably sourced.
          - generic [ref=e48]:
            - generic [ref=e49]: summer
            - generic [ref=e50]: bestseller
  - contentinfo [ref=e51]:
    - generic [ref=e52]: © 2026 Acme Fashion. All rights reserved.
  - dialog "Shopping cart" [ref=e55]:
    - banner [ref=e56]:
      - generic [ref=e57]: Your Cart (1)
      - button "Close cart" [ref=e58]:
        - img [ref=e60]
        - img [ref=e63]
    - list [ref=e66]:
      - listitem [ref=e67]:
        - generic [ref=e69]:
          - paragraph [ref=e70]: Organic Cotton T-Shirt
          - paragraph [ref=e71]: TSH-S-BLA
          - generic [ref=e72]:
            - button "Decrease quantity" [ref=e73]:
              - img [ref=e75]
              - generic [ref=e78]: "-"
            - generic [ref=e79]: "1"
            - button "Increase quantity" [ref=e80]:
              - img [ref=e82]
              - generic [ref=e85]: +
        - generic [ref=e86]:
          - paragraph [ref=e87]: 25.00 EUR
          - button "Remove Organic Cotton T-Shirt from cart" [ref=e88]:
            - img [ref=e90]
            - img [ref=e93]
    - contentinfo [ref=e95]:
      - generic [ref=e96]:
        - generic [ref=e97]:
          - term [ref=e98]: Subtotal
          - definition [ref=e99]: 25.00 EUR
        - generic [ref=e100]:
          - term [ref=e101]: Estimated total
          - definition [ref=e102]: 25.00 EUR
      - paragraph [ref=e103]: Shipping and taxes calculated at checkout
      - link "Checkout" [ref=e104] [cursor=pointer]:
        - /url: http://shop.test/checkout
      - button "Continue shopping" [ref=e105]:
        - img [ref=e107]
        - generic [ref=e110]: Continue shopping
```


Update DemoSeeder + write tests

Theme/ThemeSettings/Page/NavigationMenu seed data; HomepageTest, ProductShowTest, PageShowTest, NavigationTest

Cache-backed navigation tree + singleton theme settings loader

Phase 6: Customer Accounts

Customer auth (guard + provider from Phase 1). Livewire components: Storefront\\Account\\Auth\\Login, Register, Dashboard, Orders\\Index, Orders\\Show, Addresses\\Index. Routes /account, /account/login, /account/register, /account/orders, /account/addresses. Logout action. Tests CustomerAccountTest, AddressManagementTest.

Phase 7: Admin Panel

Admin layout (Sidebar/TopBar/Breadcrumbs/Flux). Livewire components: Admin\\Dashboard (KPIs, chart, recent orders), Admin\\Products\\Index + Form (shared form for create/edit), Admin\\Orders\\Index + Show (+ fulfillment/refund modals), Admin\\Collections\\Index + Form, Admin\\Customers\\Index + Show, Admin\\Discounts\\Index + Form, Admin\\Settings\\Index (+ Shipping + Taxes sub-tabs), Admin\\Themes\\Index (+ Editor), Admin\\Pages\\Index + Form, Admin\\Navigation\\Index, Admin\\Analytics\\Index, Admin\\Search\\Settings, Admin\\Apps\\Index + Show, Admin\\Developers\\Index. Toast notifications via Livewire events. Tests DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest.

Create storefront Blade components

Build Orders Index/Show Livewire

Orders list with pagination + order detail view with lines, addresses, fulfillments

Phase 10: Apps and Webhooks

Migrations: apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries. WebhookService (dispatch/sign/verify). DeliverWebhook job (retry backoff 60/300/1800/7200/43200s, HMAC-SHA256, circuit breaker after 5 failures). Tests WebhookDeliveryTest, WebhookSignatureTest.

Build Products admin (Index + Form)

List with search/status/pagination/bulk actions; Form for create/edit with title, description, status, vendor, product_type, tags, variants, media upload.

Analytics Index (class_exists guarded), Search Settings with reindex, Apps Index+Show stubs, Developers PAT + webhooks.

Add all routes in routes/admin.php with named routes. Enhance sidebar. Write tests: DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest. Pint + commit.

Build Orders admin (Index + Show)

Index with filters/search/date range; Show with timeline, line items, payment, fulfillment modal, refund modal, confirm-bank-transfer, cancel.

Phase 1: Foundation

Install Sanctum; set up auth guards (web/customer/sanctum); create core migrations (organizations, stores, store_domains, store_users, store_settings, modify users); create Organization/Store/StoreDomain/StoreUser/StoreSettings models + factories + seeders; add core enums (StoreStatus, StoreUserRole, StoreDomainType); create ResolveStore middleware (resolves storefront by Host header, admin by session); register storefront + admin middleware groups in bootstrap/app.php; create App\Models\Concerns\BelongsToStore trait + App\Models\Scopes\StoreScope; scaffold admin and customer auth (Livewire login/logout, customer guard, CustomerUserProvider, rate limiting); create authorization policies (Product/Order/Collection/Discount/Customer/Store/Page/Theme/Fulfillment/Refund) with role helper; admin routes under /admin/*; storefront routes on storefront host; 404 for unknown host, 503 for suspended store. Run migrations and seed. Write Pest tests for Tenancy (TenantResolutionTest, StoreIsolationTest) + Auth (AdminAuthTest, CustomerAuthTest, SanctumTokenTest). All tests must pass.

Phase 11: Polish

Accessibility audit (skip links, ARIA labels, focus mgmt in modals). Responsive breakpoints (sm/md/lg/xl). Dark mode (dark: prefix). Error pages 404/503 themed. Structured JSON logging channel. Seed data (demo store: storefront + admin + customers + products + orders).

Phase 2: Catalog

Build product catalog: migrations for products/product_options/product_option_values/product_variants/variant_option_values/inventory_items/collections/collection_products/product_media. Models with relationships + factories + seeders. Enums: ProductStatus, VariantStatus, CollectionStatus, MediaType, MediaStatus, InventoryPolicy. Services: ProductService (CRUD, status transitions), VariantMatrixService (rebuildMatrix), InventoryService (check/reserve/release/commit/restock), HandleGenerator (unique slugs per store), ProcessMediaUpload job. All services use DB transactions. Write unit tests HandleGeneratorTest, feature tests ProductCrudTest, VariantTest, InventoryTest, CollectionTest, MediaUploadTest. All pass.

Collections Index+Form with product picker/reorder. Customers Index+Show. Discounts Index+Form.

Run php artisan test, fix any failures. Run vendor/bin/pint. Fresh migrate --seed. Write + run Pest browser tests under tests/Browser/ as described in specs/08-PLAYWRIGHT-E2E-PLAN.md for full storefront + admin smoke. Playwright MCP: live review on http://shop.test demonstrating every feature (customer side: home, product, cart, checkout, order confirmation, account login/orders; admin side: login, dashboard, products CRUD, orders, discounts, settings). Fix any bugs encountered; restart the review until no bugs remain.

Covers login/register/dashboard redirects, orders list scoped to customer, address CRUD with default semantics

Enhance Dashboard with real metrics

Add date range filter, fetch orders in range, compute sales/orders/AOV/conversion, show recent orders table.

Create Phase 3 migrations

themes, theme_files, theme_settings, pages, navigation_menus, navigation_items with proper indexes and FKs

Settings (general/domains/shipping/taxes/notifications), Themes Index+Editor, Pages Index+Form, Navigation Index.

Phase 3: Themes and Storefront Layout

Migrations: themes, theme_files, theme_settings, pages, navigation_menus, navigation_items. Models + factories + seeders. Enums: ThemeStatus, PageStatus, NavigationItemType. Build storefront Blade layout (resources/views/storefront/): layouts/app.blade.php with header/announcement/main/footer/cart drawer (dark-mode ready). Pages: home, collection, product, cart, search, page. Account pages: login, register, dashboard, orders index/show, addresses. Error pages: 404, 503. Storefront components: product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination. Storefront Livewire components: Home, Collections\\Index, Collections\\Show, Products\\Show, Cart\\Show, CartDrawer, Search\\Index, Search\\Modal, Pages\\Show. NavigationService with buildTree and resolveUrl. ThemeSettings singleton. Tests to confirm pages render.

Phase 8: Search

Migrations: search_settings, search_queries, FTS5 virtual products_fts table. SearchService (search/autocomplete/syncProduct/removeProduct). ProductObserver hooking create/update/delete. Storefront Search\\Modal + Search\\Index UI. Tests SearchTest, AutocompleteTest.

Build Addresses Index + add/edit/delete

Address book with create/edit modal + set default + delete confirm

Create storefront Livewire components

Collections Index/Show, Products Show, Cart Show placeholder, Search Index/Modal, Pages Show; enhance Home

Migrations: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models + factories + seeders. Enums: CartStatus, CheckoutStatus, DiscountType, DiscountValueType, DiscountStatus, ShippingRateType, TaxMode. Services: CartService (create/addLine/updateLine/removeLine, session binding, mergeOnLogin, version increments), DiscountService (validate/calculate), ShippingCalculator (getAvailableRates/calculate), TaxCalculator (calculate/extractInclusive/addExclusive), PricingEngine (full pipeline), CheckoutService state machine, ExpireAbandonedCheckouts + CleanupAbandonedCarts jobs. ValueObjects PricingResult + TaxLine. Storefront UI: CartDrawer, Cart\\Show, Checkout\\Show (stepper), Checkout\\Confirmation. Unit tests PricingEngineTest, DiscountCalculatorTest, TaxCalculatorTest, ShippingCalculatorTest, CartVersionTest. Feature tests CartServiceTest, CartApiTest, CheckoutFlowTest, CheckoutStateTest, PricingIntegrationTest, DiscountTest, ShippingTest, TaxTest. All pass.

Create Phase 3 enums

Create Phase 3 models + factories

Theme, ThemeFile, ThemeSettings, Page, NavigationMenu, NavigationItem with relations

Phase 5: Payments, Orders, Fulfillment

Migrations: customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Models + factories + seeders. Enums: OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus. Contract PaymentProvider + MockPaymentProvider (magic card numbers for credit_card, PayPal instant, bank_transfer pending). OrderService (createFromCheckout with snapshots, generateOrderNumber sequential per store, cancel). RefundService. FulfillmentService (guard: only if paid or partially_refunded). CancelUnpaidBankTransferOrders job. Events: OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded. Tests MockPaymentProviderTest, PaymentServiceTest, BankTransferConfirmationTest, OrderCreationTest, RefundTest, FulfillmentTest.

Wire routes + error views

Update storefront.php routes and add 404/503 error views

Wire account routes + enhance dashboard

Add orders + addresses routes; show recent orders on dashboard

Phase 9: Analytics

Migrations: analytics_events, analytics_daily. AnalyticsService (track/getDailyMetrics). AggregateAnalytics daily job. Tests EventIngestionTest, AggregationTest.


Cart drawer is working correctly. Continuing through checkout.


### Ran Playwright code
```js
await page.getByRole('link', { name: 'Checkout' }).click();
```
### Page
- Page URL: http://shop.test/checkout
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-39-45-003Z.yml)


### Page
- Page URL: http://shop.test/checkout
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
```yaml
- generic [active] [ref=e111]:
  - link "Skip to content" [ref=e112] [cursor=pointer]:
    - /url: "#main-content"
  - generic [ref=e113]: Free shipping on orders over 50
  - banner [ref=e114]:
    - generic [ref=e115]:
      - link "Acme Fashion" [ref=e116] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e117]:
        - link "Collections" [ref=e118] [cursor=pointer]:
          - /url: http://shop.test/collections
        - link "Search" [ref=e119] [cursor=pointer]:
          - /url: http://shop.test/search
        - link "Account" [ref=e120] [cursor=pointer]:
          - /url: http://shop.test/account
        - link "Cart" [ref=e121] [cursor=pointer]:
          - /url: http://shop.test/cart
  - main [ref=e122]:
    - generic [ref=e123]:
      - navigation "Breadcrumb" [ref=e124]:
        - list [ref=e125]:
          - listitem [ref=e126]:
            - link "Home" [ref=e127] [cursor=pointer]:
              - /url: http://shop.test
            - img [ref=e128]
          - listitem [ref=e130]:
            - link "Cart" [ref=e131] [cursor=pointer]:
              - /url: http://shop.test/cart
            - img [ref=e132]
          - listitem [ref=e134]:
            - generic [ref=e135]: Checkout
      - generic [ref=e136]: Checkout
      - generic [ref=e137]:
        - generic [ref=e138]:
          - generic [ref=e139]:
            - generic [ref=e140]: 1. Contact & shipping
            - generic [ref=e141]:
              - generic [ref=e142]:
                - generic [ref=e143]: Email
                - textbox "Email" [ref=e145]
              - generic [ref=e146]:
                - generic [ref=e147]: First name
                - textbox "First name" [ref=e149]
              - generic [ref=e150]:
                - generic [ref=e151]: Last name
                - textbox "Last name" [ref=e153]
              - generic [ref=e154]:
                - generic [ref=e155]: Address
                - textbox "Address" [ref=e157]
              - generic [ref=e158]:
                - generic [ref=e159]: Apt / Suite
                - textbox "Apt / Suite" [ref=e161]
              - generic [ref=e162]:
                - generic [ref=e163]: City
                - textbox "City" [ref=e165]
              - generic [ref=e166]:
                - generic [ref=e167]: State / Province
                - textbox "State / Province" [ref=e169]
              - generic [ref=e170]:
                - generic [ref=e171]: Postal code
                - textbox "Postal code" [ref=e173]
              - generic [ref=e174]:
                - generic [ref=e175]: Country code
                - textbox "Country code" [ref=e177]: US
            - button "Continue" [ref=e178]:
              - img [ref=e180]
              - generic [ref=e183]: Continue
          - generic [ref=e184]:
            - generic [ref=e185]: 2. Shipping method
            - generic [ref=e186]:
              - img [ref=e188]
              - generic [ref=e192]: No shipping methods available yet. Continue past step 1 first.
          - generic [ref=e193]:
            - generic [ref=e194]: 3. Payment
            - generic [ref=e195]:
              - generic [ref=e196]:
                - radio "Credit Card" [checked] [ref=e197]
                - text: Credit Card
              - generic [ref=e198]:
                - radio "PayPal" [ref=e199]
                - text: PayPal
              - generic [ref=e200]:
                - radio "Bank Transfer" [ref=e201]
                - text: Bank Transfer
            - button "Place order - 25.00 EUR" [ref=e202]:
              - img [ref=e204]
              - generic [ref=e207]: Place order - 25.00 EUR
        - complementary [ref=e208]:
          - generic [ref=e209]:
            - generic [ref=e210]: Order summary
            - generic [ref=e211]:
              - generic [ref=e212]:
                - term [ref=e213]: Subtotal
                - definition [ref=e214]: 25.00 EUR
              - generic [ref=e215]:
                - term [ref=e216]: Shipping
                - definition [ref=e217]: 0.00 EUR
              - generic [ref=e218]:
                - term [ref=e219]: Total
                - definition [ref=e220]: 25.00 EUR
  - contentinfo [ref=e221]:
    - generic [ref=e222]: © 2026 Acme Fashion. All rights reserved.
  - generic:
    - generic:
      - dialog "Shopping cart":
        - banner:
          - generic: Your Cart (1)
          - button "Close cart":
            - generic:
              - img
            - img
        - generic:
          - list:
            - listitem:
              - generic:
                - paragraph: Organic Cotton T-Shirt
                - paragraph: TSH-S-BLA
                - generic:
                  - button "Decrease quantity":
                    - generic:
                      - img
                    - generic: "-"
                  - generic: "1"
                  - button "Increase quantity":
                    - generic:
                      - img
                    - generic: +
              - generic:
                - paragraph: 25.00 EUR
                - button "Remove Organic Cotton T-Shirt from cart":
                  - generic:
                    - img
                  - img
        - contentinfo:
          - generic:
            - generic:
              - term: Subtotal
              - definition: 25.00 EUR
            - generic:
              - term: Estimated total
              - definition: 25.00 EUR
          - paragraph: Shipping and taxes calculated at checkout
          - link "Checkout":
            - /url: http://shop.test/checkout
          - button "Continue shopping":
            - generic:
              - img
            - generic: Continue shopping
```


[
  {"name":"Email","type":"textbox","ref":"e145","value":"buyer@example.com"},
  {"name":"First name","type":"textbox","ref":"e149","value":"Billy"},
  {"name":"Last name","type":"textbox","ref":"e153","value":"Buyer"},
  {"name":"Address","type":"textbox","ref":"e157","value":"1 Shop Street"},
  {"name":"City","type":"textbox","ref":"e165","value":"Berlin"},
  {"name":"State","type":"textbox","ref":"e169","value":"BE"},
  {"name":"Postal code","type":"textbox","ref":"e173","value":"10115"},
  {"name":"Country code","type":"textbox","ref":"e177","value":"DE"}
]


Error: ### Error
[
  {
    "expected": "array",
    "code": "invalid_type",
    "path": [
      "fields"
    ],
    "message": "Invalid input: expected array, received string"
  }
]


buyer@example.com


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'Email' }).fill('buyer@example.com');
```


Billy


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'First name' }).fill('Billy');
```


Buyer


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'Last name' }).fill('Buyer');
```


1 Shop Street


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'Address' }).fill('1 Shop Street');
```


Berlin


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'City' }).fill('Berlin');
```


BE


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'State / Province' }).fill('BE');
```


### Ran Playwright code
```js
await page.getByRole('textbox', { name: 'Postal code' }).fill('10115');
```


### Ran Playwright code
```js
// Press Tab
await page.keyboard.press('Tab');
```


### Ran Playwright code
```js
await page.getByTestId('checkout-address-submit').click();
```
### Page
- Page URL: http://shop.test/checkout
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-40-17-855Z.yml)


Bash

php artisan tinker --execute='
use App\Models\Store;
use App\Models\ShippingZone;
use App\Models\ShippingRate;
use App\Models\TaxSettings;

$store = Store::first();
app()->instance("current_store", $store);

$zone = ShippingZone::query()->firstOrCreate([
    "store_id" => $store->id,
    "name" => "Germany",
], [
    "countries_json" => ["DE"],
    "regions_json" => [],
    "is_active" => true,
]);

ShippingRate::query()->firstOrCreate([
    "shipping_zone_id" => $zone->id,
    "name" => "Standard",
], [
    "rate_type" => "flat",
    "config_json" => ["amount" => 499],
    "is_active" => true,
    "min_order_amount" => null,
    "max_order_amount" => null,
]);

TaxSettings::query()->updateOrCreate([
    "store_id" => $store->id,
], [
    "mode" => "manual",
    "prices_include_tax" => false,
    "default_rate_bp" => 1900,
    "region_overrides_json" => [],
    "provider_config_json" => [],
    "updated_at" => now(),
]);

echo "seeded\n";
' 2>&1 | tail -15

Seed shipping zone and tax settings


Illuminate\Database\QueryException  SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: shipping_rates.zone_id (Connection: sqlite, Database: database/database.sqlite, SQL: insert into "shipping_rates" ("name", "config_json", "is_active") values (Standard, {"amount":499}, 1)).


php artisan tinker --execute='
use App\Models\ShippingRate;
echo json_encode((new ShippingRate)->getFillable());
echo "\n";
$cols = \Schema::getColumnListing("shipping_rates");
echo implode(",", $cols);
' 2>&1 | tail -4

Inspect ShippingRate columns and fillable


["zone_id","name","type","config_json","is_active"]
id,zone_id,name,type,config_json,is_active


php artisan tinker --execute='
use App\Models\Store;
use App\Models\ShippingZone;
use App\Models\ShippingRate;
use App\Models\TaxSettings;

$store = Store::first();
app()->instance("current_store", $store);

$zone = ShippingZone::query()->firstOrCreate(
    ["store_id" => $store->id, "name" => "Germany"],
    ["countries_json" => ["DE"], "regions_json" => [], "is_active" => true]
);

ShippingRate::query()->firstOrCreate(
    ["zone_id" => $zone->id, "name" => "Standard"],
    ["type" => "flat", "config_json" => ["amount" => 499], "is_active" => true]
);

TaxSettings::query()->updateOrCreate(
    ["store_id" => $store->id],
    ["mode" => "manual", "prices_include_tax" => false, "default_rate_bp" => 1900, "region_overrides_json" => [], "provider_config_json" => [], "updated_at" => now()]
);

echo "seeded\n";
' 2>&1 | tail -5

Retry seed with correct columns


seeded


40	{
41	    public function run(): void
42	    {
43	        $organization = Organization::query()->create([
44	            'name' => 'Acme Holdings',
45	            'billing_email' => 'billing@acme.test',
46	        ]);
47
48	        $store = Store::query()->create([
49	            'organization_id' => $organization->id,
50	            'name' => 'Acme Fashion',
51	            'handle' => 'acme-fashion',
52	            'status' => StoreStatus::Active,
53	            'default_currency' => 'EUR',
54	            'default_locale' => 'en',
55	            'timezone' => 'Europe/Berlin',
56	        ]);
57
58	        StoreDomain::query()->create([
59	            'store_id' => $store->id,
60	            'hostname' => 'shop.test',
61	            'type' => StoreDomainType::Storefront,
62	            'is_primary' => true,
63	            'tls_mode' => 'managed',
64	            'created_at' => now(),
65	        ]);
66
67	        StoreSettings::query()->create([
68	            'store_id' => $store->id,
69	            'settings_json' => [
70	                'announcement_bar' => 'Free shipping on orders over 50',
71	                'support_email' => 'support@acme.test',
72	            ],
73	            'updated_at' => now(),
74	        ]);
75
76	        $owner = User::query()->create([
77	            'name' => 'Ada Owner',
78	            'email' => 'owner@acme.test',
79	            'password' => Hash::make('password'),
80	            'email_verified_at' => now(),
81	        ]);
82
83	        \DB::table('store_users')->insert([
84	            'store_id' => $store->id,
85	            'user_id' => $owner->id,
86	            'role' => StoreUserRole::Owner->value,
87	            'created_at' => now(),
88	        ]);
89
90	        app()->instance('current_store', $store);
91
92	        Customer::query()->create([
93	            'store_id' => $store->id,
94	            'email' => 'buyer@example.com',
95	            'password' => Hash::make('password'),
96	            'first_name' => 'Billy',
97	            'last_name' => 'Buyer',
98	            'state' => 'active',
99	            'email_verified_at' => now(),

{
    public function run(): void
    {
        $organization = Organization::query()->create([
            'name' => 'Acme Holdings',
            'billing_email' => 'billing@acme.test',
        ]);

        $store = Store::query()->create([
            'organization_id' => $organization->id,
            'name' => 'Acme Fashion',
            'handle' => 'acme-fashion',
            'status' => StoreStatus::Active,
            'default_currency' => 'EUR',
            'default_locale' => 'en',
            'timezone' => 'Europe/Berlin',
        ]);

        StoreDomain::query()->create([
            'store_id' => $store->id,
            'hostname' => 'shop.test',
            'type' => StoreDomainType::Storefront,
            'is_primary' => true,
            'tls_mode' => 'managed',
            'created_at' => now(),
        ]);

        StoreSettings::query()->create([
            'store_id' => $store->id,
            'settings_json' => [
                'announcement_bar' => 'Free shipping on orders over 50',
                'support_email' => 'support@acme.test',
            ],
            'updated_at' => now(),
        ]);

        $owner = User::query()->create([
            'name' => 'Ada Owner',
            'email' => 'owner@acme.test',
            'password' => Hash::make('password'),
            'email_verified_at' => now(),
        ]);

        \DB::table('store_users')->insert([
            'store_id' => $store->id,
            'user_id' => $owner->id,
            'role' => StoreUserRole::Owner->value,
            'created_at' => now(),
        ]);

        app()->instance('current_store', $store);

        Customer::query()->create([
            'store_id' => $store->id,
            'email' => 'buyer@example.com',
            'password' => Hash::make('password'),
            'first_name' => 'Billy',
            'last_name' => 'Buyer',
            'state' => 'active',
            'email_verified_at' => now(),


100	        ]);
101
102	        $this->seedTheming($store);
103	        $this->seedCatalog($store);
104
105	        app()->forgetInstance('current_store');
106	    }
107
108	    protected function seedTheming(Store $store): void
109	    {
110	        $theme = Theme::query()->create([
111	            'store_id' => $store->id,
112	            'name' => 'Default',
113	            'version' => '1.0.0',
114	            'status' => ThemeStatus::Published,
115	            'published_at' => now(),
116	        ]);
117
118	        ThemeSettings::query()->create([
119	            'theme_id' => $theme->id,
120	            'settings_json' => [
121	                'hero' => [
122	                    'heading' => 'Elevated essentials',
123	                    'subheading' => 'Timeless pieces for modern wardrobes.',
124	                    'cta_label' => 'Shop the collection',
125	                    'cta_url' => '/collections',
126	                ],
127	                'featured_collection_handles' => ['featured'],
128	                'featured_product_handles' => [],
129	                'colors' => [

        ]);

        $this->seedTheming($store);
        $this->seedCatalog($store);

        app()->forgetInstance('current_store');
    }

    protected function seedTheming(Store $store): void
    {
        $theme = Theme::query()->create([
            'store_id' => $store->id,
            'name' => 'Default',
            'version' => '1.0.0',
            'status' => ThemeStatus::Published,
            'published_at' => now(),
        ]);

        ThemeSettings::query()->create([
            'theme_id' => $theme->id,
            'settings_json' => [
                'hero' => [
                    'heading' => 'Elevated essentials',
                    'subheading' => 'Timeless pieces for modern wardrobes.',
                    'cta_label' => 'Shop the collection',
                    'cta_url' => '/collections',
                ],
                'featured_collection_handles' => ['featured'],
                'featured_product_handles' => [],
                'colors' => [


Edit

        $this->seedTheming($store);
        $this->seedCatalog($store);

        app()->forgetInstance('current_store');
    }

        $this->seedTheming($store);
        $this->seedCatalog($store);
        $this->seedShippingAndTaxes($store);

        app()->forgetInstance('current_store');
    }

    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'prices_include_tax' => false,
                'default_rate_bp' => 1900,
                'region_overrides_json' => [],
                'provider_config_json' => [],
                'updated_at' => now(),
            ]
        );
    }


The file /Users/fabianwesner/Herd/shop/database/seeders/DemoSeeder.php has been updated successfully.

        $this->seedTheming($store);
        $this->seedCatalog($store);
        $this->seedShippingAndTaxes($store);

        app()->forgetInstance('current_store');
    }

    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'prices_include_tax' => false,
                'default_rate_bp' => 1900,
                'region_overrides_json' => [],
                'provider_config_json' => [],
                'updated_at' => now(),
            ]
        );
    }

<?php

namespace Database\Seeders;

use App\Enums\CollectionStatus;
use App\Enums\CollectionType;
use App\Enums\InventoryPolicy;
use App\Enums\MediaStatus;
use App\Enums\MediaType;
use App\Enums\NavigationItemType;
use App\Enums\PageStatus;
use App\Enums\ProductStatus;
use App\Enums\StoreDomainType;
use App\Enums\StoreStatus;
use App\Enums\StoreUserRole;
use App\Enums\ThemeStatus;
use App\Enums\VariantStatus;
use App\Models\Collection;
use App\Models\Customer;
use App\Models\InventoryItem;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Organization;
use App\Models\Page;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\StoreDomain;
use App\Models\StoreSettings;
use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class DemoSeeder extends Seeder
{
    public function run(): void
    {
        $organization = Organization::query()->create([
            'name' => 'Acme Holdings',
            'billing_email' => 'billing@acme.test',
        ]);

        $store = Store::query()->create([
            'organization_id' => $organization->id,
            'name' => 'Acme Fashion',
            'handle' => 'acme-fashion',
            'status' => StoreStatus::Active,
            'default_currency' => 'EUR',
            'default_locale' => 'en',
            'timezone' => 'Europe/Berlin',
        ]);

        StoreDomain::query()->create([
            'store_id' => $store->id,
            'hostname' => 'shop.test',
            'type' => StoreDomainType::Storefront,
            'is_primary' => true,
            'tls_mode' => 'managed',
            'created_at' => now(),
        ]);

        StoreSettings::query()->create([
            'store_id' => $store->id,
            'settings_json' => [
                'announcement_bar' => 'Free shipping on orders over 50',
                'support_email' => 'support@acme.test',
            ],
            'updated_at' => now(),
        ]);

        $owner = User::query()->create([
            'name' => 'Ada Owner',
            'email' => 'owner@acme.test',
            'password' => Hash::make('password'),
            'email_verified_at' => now(),
        ]);

        \DB::table('store_users')->insert([
            'store_id' => $store->id,
            'user_id' => $owner->id,
            'role' => StoreUserRole::Owner->value,
            'created_at' => now(),
        ]);

        app()->instance('current_store', $store);

        Customer::query()->create([
            'store_id' => $store->id,
            'email' => 'buyer@example.com',
            'password' => Hash::make('password'),
            'first_name' => 'Billy',
            'last_name' => 'Buyer',
            'state' => 'active',
            'email_verified_at' => now(),
        ]);

        $this->seedTheming($store);
        $this->seedCatalog($store);

        app()->forgetInstance('current_store');
    }

    protected function seedTheming(Store $store): void
    {
        $theme = Theme::query()->create([
            'store_id' => $store->id,
            'name' => 'Default',
            'version' => '1.0.0',
            'status' => ThemeStatus::Published,
            'published_at' => now(),
        ]);

        ThemeSettings::query()->create([
            'theme_id' => $theme->id,
            'settings_json' => [
                'hero' => [
                    'heading' => 'Elevated essentials',
                    'subheading' => 'Timeless pieces for modern wardrobes.',
                    'cta_label' => 'Shop the collection',
                    'cta_url' => '/collections',
                ],
                'featured_collection_handles' => ['featured'],
                'featured_product_handles' => [],
                'colors' => [
                    'primary' => '#111827',
                    'accent' => '#4f46e5',
                ],
                'dark_mode' => 'system',
            ],
            'updated_at' => now(),
        ]);

        Page::query()->create([
            'store_id' => $store->id,
            'title' => 'About Us',
            'handle' => 'about',
            'body_html' => '
Acme Fashion is a demo store created to showcase the platform.
',
            'status' => PageStatus::Published,
            'published_at' => now(),
        ]);

        $menu = NavigationMenu::query()->create([
            'store_id' => $store->id,
            'handle' => 'main-menu',
            'title' => 'Main menu',
        ]);

        $mainItems = [
            ['label' => 'Home', 'url' => '/'],
            ['label' => 'Collections', 'url' => '/collections'],
            ['label' => 'About', 'url' => '/pages/about'],
        ];

        foreach ($mainItems as $position => $item) {
            NavigationItem::query()->create([
                'menu_id' => $menu->id,
                'type' => NavigationItemType::Link,
                'label' => $item['label'],
                'url' => $item['url'],
                'position' => $position,
            ]);
        }
    }

    protected function seedCatalog(Store $store): void
    {
        $tshirt = Product::query()->create([
            'store_id' => $store->id,
            'title' => 'Organic Cotton T-Shirt',
            'handle' => 'organic-cotton-t-shirt',
            'status' => ProductStatus::Active,
            'description_html' => '
Soft, breathable, sustainably sourced.
',
            'vendor' => 'Acme Apparel',
            'product_type' => 'Apparel',
            'tags' => ['summer', 'bestseller'],
            'published_at' => now(),
        ]);

        $sizeOption = ProductOption::query()->create([
            'product_id' => $tshirt->id,
            'name' => 'Size',
            'position' => 0,
        ]);

        $colorOption = ProductOption::query()->create([
            'product_id' => $tshirt->id,
            'name' => 'Color',
            'position' => 1,
        ]);

        $sizeValues = [];
        foreach (['S', 'M', 'L'] as $idx => $size) {
            $sizeValues[] = ProductOptionValue::query()->create([
                'product_option_id' => $sizeOption->id,
                'value' => $size,
                'position' => $idx,
            ]);
        }

        $colorValues = [];
        foreach (['Black', 'White'] as $idx => $color) {
            $colorValues[] = ProductOptionValue::query()->create([
                'product_option_id' => $colorOption->id,
                'value' => $color,
                'position' => $idx,
            ]);
        }

        $position = 0;
        foreach ($sizeValues as $sizeIdx => $sizeValue) {
            foreach ($colorValues as $colorIdx => $colorValue) {
                $variant = ProductVariant::query()->create([
                    'product_id' => $tshirt->id,
                    'sku' => 'TSH-'.$sizeValue->value.'-'.strtoupper(substr($colorValue->value, 0, 3)),
                    'price_amount' => 2500,
                    'currency' => 'EUR',
                    'is_default' => $sizeIdx === 0 && $colorIdx === 0,
                    'position' => $position++,
                    'status' => VariantStatus::Active,
                ]);

                $variant->optionValues()->sync([$sizeValue->id, $colorValue->id]);

                InventoryItem::query()->create([
                    'store_id' => $store->id,
                    'variant_id' => $variant->id,
                    'quantity_on_hand' => 50,
                    'quantity_reserved' => 0,
                    'policy' => InventoryPolicy::Deny,
                ]);
            }
        }

        ProductMedia::query()->create([
            'product_id' => $tshirt->id,
            'type' => MediaType::Image,
            'storage_key' => 'products/tshirt-front.jpg',
            'alt_text' => 'Organic Cotton T-Shirt front',
            'width' => 1200,
            'height' => 1200,
            'mime_type' => 'image/jpeg',
            'byte_size' => 180_000,
            'position' => 0,
            'status' => MediaStatus::Ready,
            'created_at' => now(),
        ]);

        $hoodie = Product::query()->create([
            'store_id' => $store->id,
            'title' => 'Classic Pullover Hoodie',
            'handle' => 'classic-pullover-hoodie',
            'status' => ProductStatus::Active,
            'description_html' => '
Midweight fleece, relaxed fit.
',
            'vendor' => 'Acme Apparel',
            'product_type' => 'Apparel',
            'tags' => ['fall'],
            'published_at' => now(),
        ]);

        $hoodieVariant = ProductVariant::query()->create([
            'product_id' => $hoodie->id,
            'sku' => 'HD-DEFAULT',
            'price_amount' => 6000,
            'currency' => 'EUR',
            'is_default' => true,
            'position' => 0,
            'status' => VariantStatus::Active,
        ]);

        InventoryItem::query()->create([
            'store_id' => $store->id,
            'variant_id' => $hoodieVariant->id,
            'quantity_on_hand' => 30,
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ]);

        $featured = Collection::query()->create([
            'store_id' => $store->id,
            'title' => 'Featured',
            'handle' => 'featured',
            'description_html' => '
Our picks for the season.
',
            'type' => CollectionType::Manual,
            'status' => CollectionStatus::Active,
        ]);

        $featured->products()->attach([
            $tshirt->id => ['position' => 0],
            $hoodie->id => ['position' => 1],
        ]);
    }
}

+        $this->seedShippingAndTaxes($store);

+    protected function seedShippingAndTaxes(Store $store): void

+        $zone = \App\Models\ShippingZone::query()->create([

+            'countries_json' => ['DE'],

+        \App\Models\ShippingRate::query()->create([

+            'config_json' => ['amount' => 499],

+        $rowZone = \App\Models\ShippingZone::query()->create([

+        \App\Models\TaxSettings::query()->updateOrCreate(

+            ['store_id' => $store->id],

+                'prices_include_tax' => false,

+                'default_rate_bp' => 1900,

+                'region_overrides_json' => [],

+                'provider_config_json' => [],


Bash

php artisan migrate:fresh --seed --no-interaction 2>&1 | tail -5

Re-seed with shipping/tax data


      Illuminate\Database\Eloquent\Builder::create(["Germany"])

  17  database/seeders/DemoSeeder.php:104
      Database\Seeders\DemoSeeder::seedShippingAndTaxes(Object(App\Models\Store))


php artisan tinker --execute='echo json_encode((new App\Models\ShippingZone)->getFillable());'

Check ShippingZone fillable


name


Update DemoSeeder + write tests

Theme/ThemeSettings/Page/NavigationMenu seed data; HomepageTest, ProductShowTest, PageShowTest, NavigationTest

Cache-backed navigation tree + singleton theme settings loader

Phase 6: Customer Accounts

Customer auth (guard + provider from Phase 1). Livewire components: Storefront\\Account\\Auth\\Login, Register, Dashboard, Orders\\Index, Orders\\Show, Addresses\\Index. Routes /account, /account/login, /account/register, /account/orders, /account/addresses. Logout action. Tests CustomerAccountTest, AddressManagementTest.

Phase 7: Admin Panel

Admin layout (Sidebar/TopBar/Breadcrumbs/Flux). Livewire components: Admin\\Dashboard (KPIs, chart, recent orders), Admin\\Products\\Index + Form (shared form for create/edit), Admin\\Orders\\Index + Show (+ fulfillment/refund modals), Admin\\Collections\\Index + Form, Admin\\Customers\\Index + Show, Admin\\Discounts\\Index + Form, Admin\\Settings\\Index (+ Shipping + Taxes sub-tabs), Admin\\Themes\\Index (+ Editor), Admin\\Pages\\Index + Form, Admin\\Navigation\\Index, Admin\\Analytics\\Index, Admin\\Search\\Settings, Admin\\Apps\\Index + Show, Admin\\Developers\\Index. Toast notifications via Livewire events. Tests DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest.

Create storefront Blade components

Build Orders Index/Show Livewire

Orders list with pagination + order detail view with lines, addresses, fulfillments

Phase 10: Apps and Webhooks

Migrations: apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries. WebhookService (dispatch/sign/verify). DeliverWebhook job (retry backoff 60/300/1800/7200/43200s, HMAC-SHA256, circuit breaker after 5 failures). Tests WebhookDeliveryTest, WebhookSignatureTest.

Build Products admin (Index + Form)

List with search/status/pagination/bulk actions; Form for create/edit with title, description, status, vendor, product_type, tags, variants, media upload.

Analytics Index (class_exists guarded), Search Settings with reindex, Apps Index+Show stubs, Developers PAT + webhooks.

Add all routes in routes/admin.php with named routes. Enhance sidebar. Write tests: DashboardTest, ProductManagementTest, OrderManagementTest, DiscountManagementTest, SettingsTest. Pint + commit.

Build Orders admin (Index + Show)

Index with filters/search/date range; Show with timeline, line items, payment, fulfillment modal, refund modal, confirm-bank-transfer, cancel.

Phase 1: Foundation

Install Sanctum; set up auth guards (web/customer/sanctum); create core migrations (organizations, stores, store_domains, store_users, store_settings, modify users); create Organization/Store/StoreDomain/StoreUser/StoreSettings models + factories + seeders; add core enums (StoreStatus, StoreUserRole, StoreDomainType); create ResolveStore middleware (resolves storefront by Host header, admin by session); register storefront + admin middleware groups in bootstrap/app.php; create App\Models\Concerns\BelongsToStore trait + App\Models\Scopes\StoreScope; scaffold admin and customer auth (Livewire login/logout, customer guard, CustomerUserProvider, rate limiting); create authorization policies (Product/Order/Collection/Discount/Customer/Store/Page/Theme/Fulfillment/Refund) with role helper; admin routes under /admin/*; storefront routes on storefront host; 404 for unknown host, 503 for suspended store. Run migrations and seed. Write Pest tests for Tenancy (TenantResolutionTest, StoreIsolationTest) + Auth (AdminAuthTest, CustomerAuthTest, SanctumTokenTest). All tests must pass.

Phase 11: Polish

Accessibility audit (skip links, ARIA labels, focus mgmt in modals). Responsive breakpoints (sm/md/lg/xl). Dark mode (dark: prefix). Error pages 404/503 themed. Structured JSON logging channel. Seed data (demo store: storefront + admin + customers + products + orders).

Phase 2: Catalog

Build product catalog: migrations for products/product_options/product_option_values/product_variants/variant_option_values/inventory_items/collections/collection_products/product_media. Models with relationships + factories + seeders. Enums: ProductStatus, VariantStatus, CollectionStatus, MediaType, MediaStatus, InventoryPolicy. Services: ProductService (CRUD, status transitions), VariantMatrixService (rebuildMatrix), InventoryService (check/reserve/release/commit/restock), HandleGenerator (unique slugs per store), ProcessMediaUpload job. All services use DB transactions. Write unit tests HandleGeneratorTest, feature tests ProductCrudTest, VariantTest, InventoryTest, CollectionTest, MediaUploadTest. All pass.

Collections Index+Form with product picker/reorder. Customers Index+Show. Discounts Index+Form.

Run php artisan test, fix any failures. Run vendor/bin/pint. Fresh migrate --seed. Write + run Pest browser tests under tests/Browser/ as described in specs/08-PLAYWRIGHT-E2E-PLAN.md for full storefront + admin smoke. Playwright MCP: live review on http://shop.test demonstrating every feature (customer side: home, product, cart, checkout, order confirmation, account login/orders; admin side: login, dashboard, products CRUD, orders, discounts, settings). Fix any bugs encountered; restart the review until no bugs remain.

Covers login/register/dashboard redirects, orders list scoped to customer, address CRUD with default semantics

Enhance Dashboard with real metrics

Add date range filter, fetch orders in range, compute sales/orders/AOV/conversion, show recent orders table.

Create Phase 3 migrations

themes, theme_files, theme_settings, pages, navigation_menus, navigation_items with proper indexes and FKs

Settings (general/domains/shipping/taxes/notifications), Themes Index+Editor, Pages Index+Form, Navigation Index.

Phase 3: Themes and Storefront Layout

Migrations: themes, theme_files, theme_settings, pages, navigation_menus, navigation_items. Models + factories + seeders. Enums: ThemeStatus, PageStatus, NavigationItemType. Build storefront Blade layout (resources/views/storefront/): layouts/app.blade.php with header/announcement/main/footer/cart drawer (dark-mode ready). Pages: home, collection, product, cart, search, page. Account pages: login, register, dashboard, orders index/show, addresses. Error pages: 404, 503. Storefront components: product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination. Storefront Livewire components: Home, Collections\\Index, Collections\\Show, Products\\Show, Cart\\Show, CartDrawer, Search\\Index, Search\\Modal, Pages\\Show. NavigationService with buildTree and resolveUrl. ThemeSettings singleton. Tests to confirm pages render.

Phase 8: Search

Migrations: search_settings, search_queries, FTS5 virtual products_fts table. SearchService (search/autocomplete/syncProduct/removeProduct). ProductObserver hooking create/update/delete. Storefront Search\\Modal + Search\\Index UI. Tests SearchTest, AutocompleteTest.

Build Addresses Index + add/edit/delete

Address book with create/edit modal + set default + delete confirm

Create storefront Livewire components

Collections Index/Show, Products Show, Cart Show placeholder, Search Index/Modal, Pages Show; enhance Home

Migrations: carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Models + factories + seeders. Enums: CartStatus, CheckoutStatus, DiscountType, DiscountValueType, DiscountStatus, ShippingRateType, TaxMode. Services: CartService (create/addLine/updateLine/removeLine, session binding, mergeOnLogin, version increments), DiscountService (validate/calculate), ShippingCalculator (getAvailableRates/calculate), TaxCalculator (calculate/extractInclusive/addExclusive), PricingEngine (full pipeline), CheckoutService state machine, ExpireAbandonedCheckouts + CleanupAbandonedCarts jobs. ValueObjects PricingResult + TaxLine. Storefront UI: CartDrawer, Cart\\Show, Checkout\\Show (stepper), Checkout\\Confirmation. Unit tests PricingEngineTest, DiscountCalculatorTest, TaxCalculatorTest, ShippingCalculatorTest, CartVersionTest. Feature tests CartServiceTest, CartApiTest, CheckoutFlowTest, CheckoutStateTest, PricingIntegrationTest, DiscountTest, ShippingTest, TaxTest. All pass.

Create Phase 3 enums

Create Phase 3 models + factories

Theme, ThemeFile, ThemeSettings, Page, NavigationMenu, NavigationItem with relations

Phase 5: Payments, Orders, Fulfillment

Migrations: customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Models + factories + seeders. Enums: OrderStatus, FinancialStatus, FulfillmentStatus, PaymentMethod, PaymentStatus, RefundStatus, FulfillmentShipmentStatus. Contract PaymentProvider + MockPaymentProvider (magic card numbers for credit_card, PayPal instant, bank_transfer pending). OrderService (createFromCheckout with snapshots, generateOrderNumber sequential per store, cancel). RefundService. FulfillmentService (guard: only if paid or partially_refunded). CancelUnpaidBankTransferOrders job. Events: OrderCreated/OrderPaid/OrderFulfilled/OrderCancelled/OrderRefunded. Tests MockPaymentProviderTest, PaymentServiceTest, BankTransferConfirmationTest, OrderCreationTest, RefundTest, FulfillmentTest.

Wire routes + error views

Update storefront.php routes and add 404/503 error views

Wire account routes + enhance dashboard

Add orders + addresses routes; show recent orders on dashboard

Phase 9: Analytics

Migrations: analytics_events, analytics_daily. AnalyticsService (track/getDailyMetrics). AggregateAnalytics daily job. Tests EventIngestionTest, AggregationTest.


php artisan tinker --execute='
echo "Zone: ", implode(",", \Schema::getColumnListing("shipping_zones")), "\n";
echo "Rate: ", implode(",", \Schema::getColumnListing("shipping_rates")), "\n";
echo "Tax: ", implode(",", \Schema::getColumnListing("tax_settings")), "\n";
echo "ZoneFillable: ", json_encode((new App\Models\ShippingZone)->getFillable()), "\n";
echo "RateFillable: ", json_encode((new App\Models\ShippingRate)->getFillable()), "\n";
echo "TaxFillable: ", json_encode((new App\Models\TaxSettings)->getFillable()), "\n";
'

Inspect shipping/tax schema and fillable


Zone: id,store_id,name,countries_json,regions_json
Rate: id,zone_id,name,type,config_json,is_active
Tax: store_id,mode,provider,prices_include_tax,config_json
ZoneFillable: ["store_id","name","countries_json","regions_json"]
RateFillable: ["zone_id","name","type","config_json","is_active"]
TaxFillable: ["store_id","mode","provider","prices_include_tax","config_json"]


28:        if ($settings->prices_include_tax) {
65:        $config = $settings->config_json ?? [];
79:        return (int) ($config['default_rate_bps'] ?? 0);
84:        $config = $settings->config_json ?? [];


app/Services/PricingEngine.php:11:use App\Models\TaxSettings;
app/Services/PricingEngine.php:89:        $taxSettings = TaxSettings::query()->where('store_id', $store->id)->first()
app/Services/PricingEngine.php:90:            ?? $this->defaultTaxSettings($store);
app/Services/PricingEngine.php:115:    protected function defaultTaxSettings(Store $store): TaxSettings
app/Services/PricingEngine.php:117:        $settings = new TaxSettings;
app/Services/PricingEngine.php:122:        $settings->config_json = ['default_rate_bps' => 0];
app/Services/TaxCalculator.php:5:use App\Models\TaxSettings;
app/Services/TaxCalculator.php:17:    public function calculate(int $taxableAmount, TaxSettings $settings, array $address = []): array
app/Services/TaxCalculator.php:63:    protected function resolveRate(TaxSettings $settings, array $address): int
app/Services/TaxCalculator.php:79:        return (int) ($config['default_rate_bps'] ?? 0);
app/Services/TaxCalculator.php:82:    protected function resolveRateName(TaxSettings $settings, array $address): string


Edit

    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'prices_include_tax' => false,
                'default_rate_bp' => 1900,
                'region_overrides_json' => [],
                'provider_config_json' => [],
                'updated_at' => now(),
            ]
        );
    }

    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]
        );
    }


    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
            'is_active' => true,
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'prices_include_tax' => false,
                'default_rate_bp' => 1900,
                'region_overrides_json' => [],
                'provider_config_json' => [],
                'updated_at' => now(),
            ]
        );
    }

    protected function seedShippingAndTaxes(Store $store): void
    {
        $zone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $zone->id,
            'name' => 'Standard',
            'type' => 'flat',
            'config_json' => ['amount' => 499],
            'is_active' => true,
        ]);

        $rowZone = \App\Models\ShippingZone::query()->create([
            'store_id' => $store->id,
            'name' => 'Rest of World',
            'countries_json' => ['US', 'GB', 'FR', 'AT', 'CH', 'IT', 'ES', 'NL'],
            'regions_json' => [],
        ]);

        \App\Models\ShippingRate::query()->create([
            'zone_id' => $rowZone->id,
            'name' => 'International',
            'type' => 'flat',
            'config_json' => ['amount' => 1499],
            'is_active' => true,
        ]);

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]
        );
    }

-                'default_rate_bp' => 1900,

-                'region_overrides_json' => [],

-                'provider_config_json' => [],

+                    'default_rate_bps' => 1900,

+                    'default_rate_name' => 'VAT',


Bash

php artisan migrate:fresh --seed --no-interaction 2>&1 | tail -5

Re-seed


      Illuminate\Database\Eloquent\Builder::updateOrCreate([], ["manual"])

  20  database/seeders/DemoSeeder.php:104
      Database\Seeders\DemoSeeder::seedShippingAndTaxes(Object(App\Models\Store))


php artisan tinker --execute='
$m = new App\Models\TaxSettings;
echo "pk=", $m->getKeyName(), " inc=", $m->incrementing ? "yes" : "no", " pkval=", $m->getKey() ?? "null";
'

Inspect TaxSettings key


pk=store_id inc=no pkval=null


Edit

        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]
        );
    }

        $existing = \App\Models\TaxSettings::query()->where('store_id', $store->id)->first();
        if (! $existing) {
            $settings = new \App\Models\TaxSettings([
                'store_id' => $store->id,
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]);
            $settings->setAttribute('store_id', $store->id);
            $settings->save();
        }
    }


        \App\Models\TaxSettings::query()->updateOrCreate(
            ['store_id' => $store->id],
            [
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]
        );
    }

        $existing = \App\Models\TaxSettings::query()->where('store_id', $store->id)->first();
        if (! $existing) {
            $settings = new \App\Models\TaxSettings([
                'store_id' => $store->id,
                'mode' => 'manual',
                'provider' => null,
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 1900,
                    'default_rate_name' => 'VAT',
                ],
            ]);
            $settings->setAttribute('store_id', $store->id);
            $settings->save();
        }
    }

-        \App\Models\TaxSettings::query()->updateOrCreate(

-            ['store_id' => $store->id],

+        $existing = \App\Models\TaxSettings::query()->where('store_id', $store->id)->first();

+            $settings = new \App\Models\TaxSettings([

+                'store_id' => $store->id,

+            $settings->setAttribute('store_id', $store->id);


Bash

php artisan migrate:fresh --seed --no-interaction 2>&1 | tail -5

Re-seed


      Illuminate\Database\Eloquent\Model::save()

  12  database/seeders/DemoSeeder.php:104
      Database\Seeders\DemoSeeder::seedShippingAndTaxes(Object(App\Models\Store))


tail -30 /Users/fabianwesner/Herd/shop/storage/logs/laravel.log | head -15

Check latest log for detail


#28 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php(157): Illuminate\\Database\\Console\\Seeds\\SeedCommand->{closure:Illuminate\\Database\\Console\\Seeds\\SeedCommand::handle():70}()
#29 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Database/Console/Seeds/SeedCommand.php(70): Illuminate\\Database\\Eloquent\\Model::unguarded(Object(Closure))
#30 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\\Database\\Console\\Seeds\\SeedCommand->handle()
#31 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::{closure:Illuminate\\Container\\BoundMethod::call():35}()
#32 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#33 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#34 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#35 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#36 /Users/fabianwesner/Herd/shop/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArrayInput), Object(Illuminate\\Console\\OutputStyle))
#37 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArrayInput), Object(Illuminate\\Console\\OutputStyle))
#38 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Console/Concerns/CallsCommands.php(68): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArrayInput), Object(Illuminate\\Console\\OutputStyle))
#39 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Console/Concerns/CallsCommands.php(29): Illuminate\\Console\\Command->runCommand('db:seed', Array, Object(Illuminate\\Console\\OutputStyle))
#40 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/FreshCommand.php(121): Illuminate\\Console\\Command->call('db:seed', Array)
#41 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/FreshCommand.php(97): Illuminate\\Database\\Console\\Migrations\\FreshCommand->runSeeder(NULL)
#42 /Users/fabianwesner/Herd/shop/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\\Database\\Console\\Migrations\\FreshCommand->handle()


php artisan migrate:fresh --seed --no-interaction 2>&1 | tail -25

Migrate with full error


  Database\Seeders\DemoSeeder ........................................ RUNNING

   Illuminate\Database\QueryException

  SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: tax_settings.provider (Connection: sqlite, Database: database/database.sqlite, SQL: insert into "tax_settings" ("store_id", "mode", "provider", "prices_include_tax", "config_json") values (1, manual, ?, 0, {"default_rate_bps":1900,"default_rate_name":"VAT"}))

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
    834▕             $exceptionType = $this->isUniqueConstraintError($e)
    835▕                 ? UniqueConstraintViolationException::class
    836▕                 : QueryException::class;
    837▕
  ➜ 838▕             throw new $exceptionType(
    839▕                 $this->getNameWithReadWriteType(),
    840▕                 $query,
    841▕                 $this->prepareBindings($bindings),
    842▕                 $e,

      +10 vendor frames

  11  database/seeders/DemoSeeder.php:154
      Illuminate\Database\Eloquent\Model::save()

  12  database/seeders/DemoSeeder.php:104
      Database\Seeders\DemoSeeder::seedShippingAndTaxes(Object(App\Models\Store))


$settings = new \App\Models\TaxSettings([

+                'provider' => 'manual',


php artisan migrate:fresh --seed --no-interaction 2>&1 | tail -5

Re-seed with provider


   INFO  Seeding database.

  Database\Seeders\DemoSeeder ........................................ RUNNING
  Database\Seeders\DemoSeeder .................................... 561 ms DONE


Seed works. Back to Playwright to re-run the checkout flow.


### Ran Playwright code
```js
await page.goto('http://shop.test/products/organic-cotton-t-shirt');
```
### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-41-52-664Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-41-52-531Z.log#L1-L2


### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 1 errors, 0 warnings
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - link "Skip to content" [ref=e2] [cursor=pointer]:
    - /url: "#main-content"
  - generic [ref=e3]: Free shipping on orders over 50
  - banner [ref=e4]:
    - generic [ref=e5]:
      - link "Acme Fashion" [ref=e6] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e7]:
        - link "Collections" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/collections
        - link "Search" [ref=e9] [cursor=pointer]:
          - /url: http://shop.test/search
        - link "Account" [ref=e10] [cursor=pointer]:
          - /url: http://shop.test/account
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
  - main [ref=e12]:
    - generic [ref=e13]:
      - navigation "Breadcrumb" [ref=e14]:
        - list [ref=e15]:
          - listitem [ref=e16]:
            - link "Home" [ref=e17] [cursor=pointer]:
              - /url: http://shop.test
            - img [ref=e18]
          - listitem [ref=e20]:
            - generic [ref=e21]: Products
            - img [ref=e22]
          - listitem [ref=e24]:
            - generic [ref=e25]: Organic Cotton T-Shirt
      - generic [ref=e26]:
        - img "Organic Cotton T-Shirt front" [ref=e29]
        - generic [ref=e30]:
          - generic [ref=e31]: Organic Cotton T-Shirt
          - generic [ref=e33]: 25.00 EUR
          - generic [ref=e34]:
            - generic [ref=e35]: Variant
            - combobox "Variant" [ref=e36]:
              - option "TSH-S-BLA" [selected]
              - option "TSH-S-WHI"
              - option "TSH-M-BLA"
              - option "TSH-M-WHI"
              - option "TSH-L-BLA"
              - option "TSH-L-WHI"
          - generic [ref=e37]:
            - generic [ref=e38]: Quantity
            - spinbutton "Quantity" [ref=e39]: "1"
          - button "Add to cart" [ref=e40]:
            - img [ref=e42]
            - generic [ref=e45]: Add to cart
          - paragraph [ref=e47]: Soft, breathable, sustainably sourced.
          - generic [ref=e48]:
            - generic [ref=e49]: summer
            - generic [ref=e50]: bestseller
  - contentinfo [ref=e51]:
    - generic [ref=e52]: © 2026 Acme Fashion. All rights reserved.
  - generic:
    - generic:
      - dialog "Shopping cart":
        - banner:
          - generic: Your Cart (0)
          - button "Close cart":
            - generic:
              - img
            - img
        - generic:
          - generic:
            - img
            - paragraph: Your cart is empty
            - button "Continue shopping":
              - generic:
                - img
              - generic: Continue shopping
```


### Ran Playwright code
```js
await page.getByTestId('add-to-cart').click();
```
### Page
- Page URL: http://shop.test/products/organic-cotton-t-shirt
- Page Title: Acme Fashion
- Console: 2 errors, 0 warnings
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-41-59-687Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-41-52-531Z.log#L3


### Ran Playwright code
```js
await page.goto('http://shop.test/checkout');
```
### Page
- Page URL: http://shop.test/checkout
- Page Title: Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-04-18T08-42-02-779Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-04-18T08-42-02-646Z.log#L1


() => {
  const form = {};
  document.querySelectorAll('input[wire\\:model], textarea[wire\\:model]').forEach(el => {
    form[el.getAttribute('wire:model') || el.name || el.id] = {
      type: el.type,
      id: el.id,
      value: el.value,
      label: el.closest('[data-flux-field]')?.querySelector('[data-flux-label]')?.textContent?.trim(),
    };
  });
  return form;
}


### Result
{
  "email": {
    "type": "email",
    "id": "",
    "value": "",
    "label": "Email"
  },
  "address.first_name": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "First name"
  },
  "address.last_name": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "Last name"
  },
  "address.address1": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "Address"
  },
  "address.address2": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "Apt / Suite"
  },
  "address.city": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "City"
  },
  "address.province_code": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "State / Province"
  },
  "address.zip": {
    "type": "text",
    "id": "",
    "value": "",
    "label": "Postal code"
  },
  "address.country_code": {
    "type": "text",
    "id": "",
    "value": "US",
    "label": "Country code"
  },
  "paymentMethod": {
    "type": "radio",
    "id": "",
    "value": "bank_transfer"
  }
}
### Ran Playwright code
```js
await page.evaluate('() => {\n  const form = {};\n  document.querySelectorAll(\'input[wire\\\\:model], textarea[wire\\\\:model]\').forEach(el => {\n    form[el.getAttribute(\'wire:model\') || el.name || el.id] = {\n      type: el.type,\n      id: el.id,\n      value: el.value,\n      label: el.closest(\'[data-flux-field]\')?.querySelector(\'[data-flux-label]\')?.textContent?.trim(),\n    };\n  });\n  return form;\n}');
```