userType: external

gitBranch: claude-subagents

slug: wise-shimmying-pretzel

type: message

role: assistant

type: thinking

thinking:
Phase 3 agent completed successfully. Key results:
- All 128 tests pass (219 assertions)
- Pint formatting clean
- Fixed pre-existing test failures (route('home') -> url('/'), ExampleTest updated with RefreshDatabase)
- Created all Phase 3 content: themes, pages, navigation, storefront layout, Livewire components
- Updated DatabaseSeeder with themes, pages, navigation data
- Migration + seed works cleanly

Now I need to:
1. Commit Phases 2 & 3
2. Update progress.md
3. Mark tasks as completed
4. Launch Phase 4 (Cart, Checkout, Discounts, Shipping, Taxes)

5. Let me check the current state before committing.

service_tier: standard

type: assistant


type: text

text:
Phase 3 agent completed successfully. All **128 tests pass** (219 assertions), Pint clean. The agent also fixed pre-existing test failures (`route('home')` references and ExampleTest).

Let me commit this work and continue.


Bash

description: Check all uncommitted changes


type: user

role: user

content:
 M app/Livewire/Storefront/Home.php
 M app/Providers/AppServiceProvider.php
 M database/seeders/DatabaseSeeder.php
 M resources/views/layouts/auth/card.blade.php
 M resources/views/layouts/auth/simple.blade.php
 M resources/views/layouts/auth/split.blade.php
 M resources/views/livewire/storefront/home.blade.php
 M routes/storefront.php
 M specs/progress.md
 M tests/Feature/Auth/AuthenticationTest.php
 M tests/Feature/ExampleTest.php
?? app/Enums/CollectionStatus.php
?? app/Enums/CollectionType.php
?? app/Enums/InventoryPolicy.php
?? app/Enums/MediaStatus.php
?? app/Enums/MediaType.php
?? app/Enums/NavigationItemType.php
?? app/Enums/PageStatus.php
?? app/Enums/ProductStatus.php
?? app/Enums/ThemeStatus.php
?? app/Enums/VariantStatus.php
?? app/Exceptions/
?? app/Jobs/
?? app/Livewire/Storefront/Cart/
?? app/Livewire/Storefront/CartDrawer.php
?? app/Livewire/Storefront/Collections/
?? app/Livewire/Storefront/Pages/
?? app/Livewire/Storefront/Products/
?? app/Livewire/Storefront/Search/
?? app/Models/Collection.php
?? app/Models/InventoryItem.php
?? app/Models/NavigationItem.php
?? app/Models/NavigationMenu.php
?? app/Models/Page.php
?? app/Models/Product.php
?? app/Models/ProductMedia.php
?? app/Models/ProductOption.php
?? app/Models/ProductOptionValue.php
?? app/Models/ProductVariant.php
?? app/Models/Theme.php
?? app/Models/ThemeFile.php
?? app/Models/ThemeSettings.php
?? app/Services/
?? app/Support/
?? database/factories/CollectionFactory.php
?? database/factories/InventoryItemFactory.php
?? database/factories/NavigationItemFactory.php
?? database/factories/NavigationMenuFactory.php
?? database/factories/PageFactory.php
?? database/factories/ProductFactory.php
?? database/factories/ProductMediaFactory.php
?? database/factories/ProductOptionFactory.php
?? database/factories/ProductOptionValueFactory.php
?? database/factories/ProductVariantFactory.php
?? database/factories/ThemeFactory.php
?? database/factories/ThemeFileFactory.php
?? database/factories/ThemeSettingsFactory.php
?? database/migrations/2025_01_01_000100_create_products_table.php
?? database/migrations/2025_01_01_000110_create_product_options_table.php
?? database/migrations/2025_01_01_000120_create_product_option_values_table.php
?? database/migrations/2025_01_01_000130_create_product_variants_table.php
?? database/migrations/2025_01_01_000140_create_variant_option_values_table.php
?? database/migrations/2025_01_01_000150_create_inventory_items_table.php
?? database/migrations/2025_01_01_000160_create_collections_table.php
?? database/migrations/2025_01_01_000170_create_collection_products_table.php
?? database/migrations/2025_01_01_000180_create_product_media_table.php
?? database/migrations/2025_01_01_000200_create_themes_table.php
?? database/migrations/2025_01_01_000210_create_theme_files_table.php
?? database/migrations/2025_01_01_000220_create_theme_settings_table.php
?? database/migrations/2025_01_01_000230_create_pages_table.php
?? database/migrations/2025_01_01_000240_create_navigation_menus_table.php
?? database/migrations/2025_01_01_000250_create_navigation_items_table.php
?? resources/views/components/storefront/
?? resources/views/livewire/storefront/cart-drawer.blade.php
?? resources/views/livewire/storefront/cart/
?? resources/views/livewire/storefront/collections/
?? resources/views/livewire/storefront/pages/
?? resources/views/livewire/storefront/products/
?? resources/views/livewire/storefront/search/
?? resources/views/storefront/
?? tests/Feature/Catalog/
?? tests/Feature/Storefront/


description: Check recent commits


content:
7434bef Phase 1: Foundation - migrations, models, middleware, auth
5cd4287 Initial


thinking:
Good. Now I need to:
1. Update progress.md to mark Phase 2 and 3 as done
2. Stage all files and commit
3. Then start Phase 4

Let me update progress.md and commit everything.


text: Now let me update progress.md, commit Phase 2 & 3, and launch Phase 4.


type: progress

hookEvent: PostToolUse

command: callback


content:
     1→# Implementation Progress
     2→
     3→## Phase 1: Foundation (Migrations, Models, Middleware, Auth) - DONE
     4→- [x] Step 1.1: Environment and Config
     5→- [x] Step 1.2: Core Migrations
     6→- [x] Step 1.3: Core Models
     7→- [x] Step 1.4: Enums
     8→- [x] Step 1.5: Tenant Resolution Middleware
     9→- [x] Step 1.6: BelongsToStore Trait and Global Scope
    10→- [x] Step 1.7: Authentication
    11→- [x] Step 1.8: Authorization
    12→
    13→## Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
    14→- [ ] Step 2.1: Migrations
    15→- [ ] Step 2.2: Models with Relationships
    16→- [ ] Step 2.3: Product Service
    17→- [ ] Step 2.4: Inventory Service
    18→- [ ] Step 2.5: Media Upload
    19→
    20→## Phase 3: Themes, Pages, Navigation, Storefront Layout
    21→- [ ] Step 3.1: Migrations
    22→- [ ] Step 3.2: Models
    23→- [ ] Step 3.3: Storefront Blade Layout
    24→- [ ] Step 3.4: Storefront Livewire Components
    25→- [ ] Step 3.5: Navigation Service
    26→
    27→## Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
    28→- [ ] Step 4.1: Migrations
    29→- [ ] Step 4.2: Models
    30→- [ ] Step 4.3: Cart Service
    31→- [ ] Step 4.4: Discount Service
    32→- [ ] Step 4.5: Shipping Calculator
    33→- [ ] Step 4.6: Tax Calculator
    34→- [ ] Step 4.7: Pricing Engine
    35→- [ ] Step 4.8: Checkout State Machine
    36→- [ ] Step 4.9: Storefront Cart/Checkout UI
    37→
    38→## Phase 5: Payments, Orders, Fulfillment
    39→- [ ] Step 5.1: Migrations
    40→- [ ] Step 5.2: Models
    41→- [ ] Step 5.3: Payment Service (Mock PSP)
    42→- [ ] Step 5.3b: Admin Payment Confirmation
    43→- [ ] Step 5.4: Order Service
    44→- [ ] Step 5.5: Refund Service
    45→- [ ] Step 5.6: Fulfillment Service
    46→
    47→## Phase 6: Customer Accounts
    48→- [ ] Step 6.1: Customer Auth
    49→- [ ] Step 6.2: Customer Account Pages
    50→
    51→## Phase 7: Admin Panel
    52→- [ ] Step 7.1: Admin Layout
    53→- [ ] Step 7.2: Dashboard
    54→- [ ] Step 7.3: Product Management
    55→- [ ] Step 7.4: Order Management
    56→- [ ] Step 7.5: Other Admin Sections
    57→
    58→## Phase 8: Search
    59→- [ ] Step 8.1: Migrations
    60→- [ ] Step 8.2: Search Service
    61→- [ ] Step 8.3: Search UI
    62→
    63→## Phase 9: Analytics
    64→- [ ] Step 9.1: Migrations
    65→- [ ] Step 9.2: Analytics Service
    66→
    67→## Phase 10: Apps and Webhooks
    68→- [ ] Step 10.1: Migrations
    69→- [ ] Step 10.2: Webhook Service
    70→
    71→## Phase 11: Polish
    72→- [ ] Accessibility, responsive, dark mode, error pages, logging, seeds
    73→
    74→## Phase 12: Full Test Suite
    75→- [ ] All unit tests pass
    76→- [ ] All feature tests pass
    77→- [ ] All Playwright E2E tests pass
    78→- [ ] Code style (Pint) passes
    79→- [ ] Fresh migration + seed works
    80→

content:
# Implementation Progress

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

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

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

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

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

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

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

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

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

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

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

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


Edit

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

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

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

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

speed: standard


content: The file /Users/fabianwesner/Herd/shop/specs/progress.md has been updated successfully.

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

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

originalFile:
# Implementation Progress

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

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

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

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

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

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

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

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

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

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

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

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

lines: -## Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)

lines: -- [ ] Step 2.2: Models with Relationships

lines: +## Phase 2: Catalog (Products, Variants, Inventory, Collections, Media) - DONE

lines: +- [x] Step 2.2: Models with Relationships

lines: -## Phase 3: Themes, Pages, Navigation, Storefront Layout

lines: -- [ ] Step 3.3: Storefront Blade Layout

lines: -- [ ] Step 3.4: Storefront Livewire Components

lines: +## Phase 3: Themes, Pages, Navigation, Storefront Layout - DONE

lines: +- [x] Step 3.3: Storefront Blade Layout

lines: +- [x] Step 3.4: Storefront Livewire Components


text: Now let me verify all tests pass, then commit and immediately launch Phase 4.


Bash

description: Verify full test suite passes before commit


output:
....
..
....
  Tests:    128 passed (219 assertions)
  Duration: 2.58s


content:
  ............................................................................
  ....................................................

  Tests:    128 passed (219 assertions)
  Duration: 2.58s


text: All 128 tests pass. Let me commit and then launch Phase 4.


command: git add app/Enums/CollectionStatus.php app/Enums/CollectionType.php app/Enums/InventoryPolicy.php app/Enums/MediaStatus.php app/Enums/MediaType.php app/Enums/NavigationItemType.php app/Enums/PageStatus.php app/Enums/ProductStatus.php app/Enums/ThemeStatus.php app/Enums/VariantStatus.php app/Exceptions/ app/Jobs/ app/Livewire/Storefront/ app/Models/Collection.php app/Models/InventoryItem.php app/Models/NavigationItem.php app/Models/NavigationMenu.php app/Models/Page.php app/Models/Product.php app/Models/ProductMedia.php app/Models/ProductOption.php app/Models/ProductOptionValue.php app/Models/ProductVariant.php app/Models/Theme.php app/Models/ThemeFile.php app/Models/ThemeSettings.php app/Services/ app/Support/ app/Providers/AppServiceProvider.php database/factories/ database/migrations/2025_01_01_0001*.php database/migrations/2025_01_01_0002*.php database/seeders/DatabaseSeeder.php resources/views/components/storefront/ resources/views/livewire/storefront/ resources/views/storefront/ resources/views/layouts/auth/ routes/storefront.php specs/progress.md tests/Feature/Catalog/ tests/Feature/Storefront/ tests/Feature/Auth/AuthenticationTest.php tests/Feature/ExampleTest.php

description: Stage all Phase 2 and Phase 3 files


command:
git commit -m "$(cat <<'EOF'
Phase 2 & 3: Catalog system, themes, storefront layout

- 9 catalog migrations (products, options, variants, inventory, collections, media)
- 6 theme/content migrations (themes, theme_files, theme_settings, pages, navigation)
- 10 enums (ProductStatus, VariantStatus, CollectionStatus, etc.)
- 14 models with factories and relationships
- ProductService, VariantMatrixService, InventoryService
- NavigationService, ThemeSettingsService, HandleGenerator
- Storefront Blade layout with header, footer, announcement bar
- Storefront Livewire components (home, collections, products, pages, search, cart)
- Blade components (product-card, price, badge, quantity-selector, breadcrumbs)
- DatabaseSeeder extended with theme, pages, navigation menus
- Fixed pre-existing test failures (route('home') references, ExampleTest)
- 128 tests passing (219 assertions), Pint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"

description: Commit Phase 2 and Phase 3 changes


toolUseResult:
Error: Exit code 1
(eval):1: can't create temp file for here document: operation not permitted
Aborting commit due to empty commit message.


command:
git commit -m "Phase 2 & 3: Catalog system, themes, storefront layout

- 9 catalog migrations (products, options, variants, inventory, collections, media)
- 6 theme/content migrations (themes, theme_files, theme_settings, pages, navigation)
- 10 enums (ProductStatus, VariantStatus, CollectionStatus, etc.)
- 14 models with factories and relationships
- ProductService, VariantMatrixService, InventoryService
- NavigationService, ThemeSettingsService, HandleGenerator
- Storefront Blade layout with header, footer, announcement bar
- Storefront Livewire components (home, collections, products, pages, search, cart)
- Fixed pre-existing test failures (route home references, ExampleTest)
- 128 tests passing (219 assertions), Pint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

description: Commit Phase 2 and 3 with inline message


content:
[claude-subagents 258d429] Phase 2 & 3: Catalog system, themes, storefront layout
 105 files changed, 5908 insertions(+), 31 deletions(-)
 create mode 100644 app/Enums/CollectionStatus.php
 create mode 100644 app/Enums/CollectionType.php
 create mode 100644 app/Enums/InventoryPolicy.php
 create mode 100644 app/Enums/MediaStatus.php
 create mode 100644 app/Enums/MediaType.php
 create mode 100644 app/Enums/NavigationItemType.php
 create mode 100644 app/Enums/PageStatus.php
 create mode 100644 app/Enums/ProductStatus.php
 create mode 100644 app/Enums/ThemeStatus.php
 create mode 100644 app/Enums/VariantStatus.php
 create mode 100644 app/Exceptions/InsufficientInventoryException.php
 create mode 100644 app/Exceptions/InvalidProductTransitionException.php
 create mode 100644 app/Jobs/ProcessMediaUpload.php
 create mode 100644 app/Livewire/Storefront/Cart/Show.php
 create mode 100644 app/Livewire/Storefront/CartDrawer.php
 create mode 100644 app/Livewire/Storefront/Collections/Index.php
 create mode 100644 app/Livewire/Storefront/Collections/Show.php
 create mode 100644 app/Livewire/Storefront/Pages/Show.php
 create mode 100644 app/Livewire/Storefront/Products/Show.php
 create mode 100644 app/Livewire/Storefront/Search/Index.php
 create mode 100644 app/Models/Collection.php
 create mode 100644 app/Models/InventoryItem.php
 create mode 100644 app/Models/NavigationItem.php
 create mode 100644 app/Models/NavigationMenu.php
 create mode 100644 app/Models/Page.php
 create mode 100644 app/Models/Product.php
 create mode 100644 app/Models/ProductMedia.php
 create mode 100644 app/Models/ProductOption.php
 create mode 100644 app/Models/ProductOptionValue.php
 create mode 100644 app/Models/ProductVariant.php
 create mode 100644 app/Models/Theme.php
 create mode 100644 app/Models/ThemeFile.php
 create mode 100644 app/Models/ThemeSettings.php
 create mode 100644 app/Services/InventoryService.php
 create mode 100644 app/Services/NavigationService.php
 create mode 100644 app/Services/ProductService.php
 create mode 100644 app/Services/ThemeSettingsService.php
 create mode 100644 app/Services/VariantMatrixService.php
 create mode 100644 app/Support/HandleGenerator.php
 create mode 100644 database/factories/CollectionFactory.php
 create mode 100644 database/factories/InventoryItemFactory.php
 create mode 100644 database/factories/NavigationItemFactory.php
 create mode 100644 database/factories/NavigationMenuFactory.php
 create mode 100644 database/factories/PageFactory.php
 create mode 100644 database/factories/ProductFactory.php
 create mode 100644 database/factories/ProductMediaFactory.php
 create mode 100644 database/factories/ProductOptionFactory.php
 create mode 100644 database/factories/ProductOptionValueFactory.php
 create mode 100644 database/factories/ProductVariantFactory.php
 create mode 100644 database/factories/ThemeFactory.php
 create mode 100644 database/factories/ThemeFileFactory.php
 create mode 100644 database/factories/ThemeSettingsFactory.php
 create mode 100644 database/migrations/2025_01_01_000100_create_products_table.php
 create mode 100644 database/migrations/2025_01_01_000110_create_product_options_table.php
 create mode 100644 database/migrations/2025_01_01_000120_create_product_option_values_table.php
 create mode 100644 database/migrations/2025_01_01_000130_create_product_variants_table.php
 create mode 100644 database/migrations/2025_01_01_000140_create_variant_option_values_table.php
 create mode 100644 database/migrations/2025_01_01_000150_create_inventory_items_table.php
 create mode 100644 database/migrations/2025_01_01_000160_create_collections_table.php
 create mode 100644 database/migrations/2025_01_01_000170_create_collection_products_table.php
 create mode 100644 database/migrations/2025_01_01_000180_create_product_media_table.php
 create mode 100644 database/migrations/2025_01_01_000200_create_themes_table.php
 create mode 100644 database/migrations/2025_01_01_000210_create_theme_files_table.php
 create mode 100644 database/migrations/2025_01_01_000220_create_theme_settings_table.php
 create mode 100644 database/migrations/2025_01_01_000230_create_pages_table.php
 create mode 100644 database/migrations/2025_01_01_000240_create_navigation_menus_table.php
 create mode 100644 database/migrations/2025_01_01_000250_create_navigation_items_table.php
 create mode 100644 resources/views/components/storefront/badge.blade.php
 create mode 100644 resources/views/components/storefront/breadcrumbs.blade.php
 create mode 100644 resources/views/components/storefront/pagination.blade.php
 create mode 100644 resources/views/components/storefront/price.blade.php
 create mode 100644 resources/views/components/storefront/product-card.blade.php
 create mode 100644 resources/views/components/storefront/quantity-selector.blade.php
 create mode 100644 resources/views/livewire/storefront/cart-drawer.blade.php
 create mode 100644 resources/views/livewire/storefront/cart/show.blade.php
 create mode 100644 resources/views/livewire/storefront/collections/index.blade.php
 create mode 100644 resources/views/livewire/storefront/collections/show.blade.php
 create mode 100644 resources/views/livewire/storefront/pages/show.blade.php
 create mode 100644 resources/views/livewire/storefront/products/show.blade.php
 create mode 100644 resources/views/livewire/storefront/search/index.blade.php
 create mode 100644 resources/views/storefront/layouts/app.blade.php
 create mode 100644 tests/Feature/Catalog/HandleGeneratorTest.php
 create mode 100644 tests/Feature/Catalog/InventoryServiceTest.php
 create mode 100644 tests/Feature/Catalog/ProcessMediaUploadTest.php
 create mode 100644 tests/Feature/Catalog/ProductModelTest.php
 create mode 100644 tests/Feature/Catalog/ProductServiceTest.php
 create mode 100644 tests/Feature/Catalog/VariantMatrixServiceTest.php
 create mode 100644 tests/Feature/Storefront/NavigationModelTest.php
 create mode 100644 tests/Feature/Storefront/NavigationServiceTest.php
 create mode 100644 tests/Feature/Storefront/PageModelTest.php
 create mode 100644 tests/Feature/Storefront/PriceComponentTest.php
 create mode 100644 tests/Feature/Storefront/StorefrontRoutesTest.php
 create mode 100644 tests/Feature/Storefront/ThemeModelTest.php
 create mode 100644 tests/Feature/Storefront/ThemeSettingsServiceTest.php


thinking:
Committed. Now let me update the task list and launch Phase 4. I need to read the spec files for Phase 4 to give the agent the right context. Let me read the relevant specs.

Let me now launch Phase 4 (Cart, Checkout, Discounts, Shipping, Taxes) as a background agent and simultaneously read the spec files needed. Actually, the agent can read the spec files itself. Let me launch Phase 4 and Phase 5 agents. But Phase 5 depends on Phase 4 (the orders table references carts, discounts, etc.), so I can't run them in parallel.

Let me launch Phase 4 now.


text: Committed as `258d429`. Now launching Phase 4 (Cart, Checkout, Discounts, Shipping, Taxes) - this is the largest phase.


content: Updated task #2 status

to: completed


content: Updated task #3 status


content: Updated task #4 status

from: pending


Task

description: Phase 4: Cart, Checkout, Discounts

prompt:
You are implementing Phase 4 (Cart, Checkout, Discounts, Shipping, Taxes) of an e-commerce shop system in Laravel 12 with Livewire v4, Flux UI Free v2, SQLite, and Pest v4.

## Context
- Phases 1-3 are complete (Foundation, Catalog, Themes/Storefront).
- Working directory: /Users/fabianwesner/Herd/shop
- All 128 existing tests pass.
- Read the specs before implementing: `specs/01-DATABASE-SCHEMA.md`, `specs/05-BUSINESS-LOGIC.md`, `specs/09-IMPLEMENTATION-ROADMAP.md`

## What to implement (Phase 4)

### Step 4.1: Migrations
Create migrations numbered 000300-000380 for these tables (read specs/01-DATABASE-SCHEMA.md for exact schemas):
- `carts` - Cart with store_id, customer_id (nullable), session_id, status, currency, version (for optimistic concurrency), totals
- `cart_items` - Items in cart with product_variant_id, quantity, unit_price, line_total
- `discounts` - Discount codes with store_id, code, type (percentage/fixed), value, usage limits, date ranges
- `discount_rules` - JSON-based rules for auto-applying discounts (min_amount, min_quantity, collection_ids, product_ids)
- `shipping_zones` - Shipping zones per store with name
- `shipping_rates` - Rates per zone (flat, weight, price-based) with min/max thresholds
- `tax_zones` - Tax zones per store
- `tax_rates` - Tax rates with percentage, inclusive/exclusive flag
- `checkouts` - Checkout sessions linking to cart, with state machine (information, shipping, payment, processing, completed, abandoned)

### Step 4.2: Models
Create Eloquent models with proper relationships, casts, and BelongsToStore trait where applicable:
- Cart, CartItem, Discount, DiscountRule, ShippingZone, ShippingRate, TaxZone, TaxRate, Checkout
- Create factories for each model
- Create enums: CartStatus, DiscountType, DiscountStatus, ShippingRateType, CheckoutState

### Step 4.3: Cart Service (app/Services/CartService.php)
Implement the cart service with these methods:
- `getOrCreateCart(Store $store, ?Customer $customer, ?string $sessionId): Cart`
- `addItem(Cart $cart, ProductVariant $variant, int $quantity): CartItem` - with inventory availability check
- `updateItemQuantity(CartItem $item, int $quantity): CartItem` - with optimistic locking via cart version
- `removeItem(CartItem $item): void`
- `applyDiscountCode(Cart $cart, string $code): Cart` - validates code, checks rules
- `removeDiscount(Cart $cart): Cart`
- `mergeGuestCart(Cart $guestCart, Customer $customer): Cart` - merge on login
- `recalculateTotals(Cart $cart): Cart` - subtotal, discount, tax, shipping, total
- Use optimistic concurrency: increment cart.version on every mutation, throw if stale

### Step 4.4: Discount Service (app/Services/DiscountService.php)
- `validateCode(Store $store, string $code): Discount` - check active, date range, usage limits
- `calculateDiscount(Discount $discount, Cart $cart): int` - apply percentage or fixed
- `checkRules(Discount $discount, Cart $cart): bool` - evaluate discount_rules JSON
- `incrementUsage(Discount $discount): void`

### Step 4.5: Shipping Calculator (app/Services/ShippingCalculator.php)
- `getAvailableRates(Cart $cart, array $shippingAddress): Collection` - match zones by country/region
- `calculateRate(ShippingRate $rate, Cart $cart): int` - flat, weight-based, or price-based

### Step 4.6: Tax Calculator (app/Services/TaxCalculator.php)
- `calculateTax(Cart $cart, array $address): int` - match zones, apply rates
- `getTaxBreakdown(Cart $cart, array $address): array` - return per-rate breakdown
- Support tax-inclusive and tax-exclusive pricing

### Step 4.7: Pricing Engine (app/Services/PricingEngine.php)
Orchestrates the full price calculation:
- Takes a Cart, applies discount, calculates shipping, calculates tax
- Returns a PriceBreakdown value object with subtotal, discount_amount, shipping_amount, tax_amount, total
- All amounts in INTEGER minor units (cents)

### Step 4.8: Checkout State Machine (app/Services/CheckoutService.php)
Implement checkout flow with state transitions:
- States: information -> shipping -> payment -> processing -> completed (also abandoned)
- `createCheckout(Cart $cart): Checkout`
- `updateInformation(Checkout $checkout, array $data): Checkout` - email, shipping/billing address
- `selectShippingRate(Checkout $checkout, ShippingRate $rate): Checkout`
- `transitionTo(Checkout $checkout, CheckoutState $state): Checkout` - validate allowed transitions
- `abandonCheckout(Checkout $checkout): void`
- Validate state transitions are valid (e.g. can't go from information directly to payment)

### Step 4.9: Storefront Cart/Checkout UI (Livewire components)
Update the existing storefront Livewire components:
- Update `app/Livewire/Storefront/CartDrawer.php` - slide-out cart drawer with items, quantity adjustment, discount code, totals
- Update `app/Livewire/Storefront/Cart/Show.php` - full cart page
- Create `app/Livewire/Storefront/Checkout/Information.php` - email, shipping address form
- Create `app/Livewire/Storefront/Checkout/Shipping.php` - shipping rate selection
- Create `app/Livewire/Storefront/Checkout/Payment.php` - payment method selection (mock)
- Create `app/Livewire/Storefront/Checkout/Complete.php` - order confirmation
- Add checkout routes to routes/storefront.php

## Testing Requirements
Write comprehensive Pest tests for:
- CartService: add/update/remove items, apply discount, merge carts, optimistic concurrency
- DiscountService: validation, calculation, rule checking
- ShippingCalculator: zone matching, rate calculation
- TaxCalculator: tax calculation, inclusive/exclusive
- PricingEngine: full price breakdown
- CheckoutService: state transitions, validation
- Target: at least 60 new tests

## Important Rules
- Use `php artisan make:model` commands where possible (with `--no-interaction`)
- All monetary amounts as INTEGER in minor units (cents)
- Use BelongsToStore trait on store-scoped models
- Use existing patterns from Phase 1-3 code for consistency
- Run `vendor/bin/pint --dirty --format agent` before finishing
- Run `php artisan test --compact` to verify all tests pass (both old and new)
- DO NOT modify existing test files
- DO NOT create any git commits
- Read existing models, services, and tests to understand patterns before writing code


text:
Async agent launched successfully.
agentId: a2095c7 (internal ID - do not mention to user. Use to resume later if needed.)
The agent is working in the background. You will be notified automatically when it completes.
Continue with other tasks.
output_file: /private/tmp/claude-501/-Users-fabianwesner-Herd-shop/tasks/a2095c7.output
To check progress before completion (optional), use Read or Bash tail on the output file.

description: Phase 4: Cart, Checkout, Discounts

prompt:
You are implementing Phase 4 (Cart, Checkout, Discounts, Shipping, Taxes) of an e-commerce shop system in Laravel 12 with Livewire v4, Flux UI Free v2, SQLite, and Pest v4.

## Context
- Phases 1-3 are complete (Foundation, Catalog, Themes/Storefront).
- Working directory: /Users/fabianwesner/Herd/shop
- All 128 existing tests pass.
- Read the specs before implementing: `specs/01-DATABASE-SCHEMA.md`, `specs/05-BUSINESS-LOGIC.md`, `specs/09-IMPLEMENTATION-ROADMAP.md`

## What to implement (Phase 4)

### Step 4.1: Migrations
Create migrations numbered 000300-000380 for these tables (read specs/01-DATABASE-SCHEMA.md for exact schemas):
- `carts` - Cart with store_id, customer_id (nullable), session_id, status, currency, version (for optimistic concurrency), totals
- `cart_items` - Items in cart with product_variant_id, quantity, unit_price, line_total
- `discounts` - Discount codes with store_id, code, type (percentage/fixed), value, usage limits, date ranges
- `discount_rules` - JSON-based rules for auto-applying discounts (min_amount, min_quantity, collection_ids, product_ids)
- `shipping_zones` - Shipping zones per store with name
- `shipping_rates` - Rates per zone (flat, weight, price-based) with min/max thresholds
- `tax_zones` - Tax zones per store
- `tax_rates` - Tax rates with percentage, inclusive/exclusive flag
- `checkouts` - Checkout sessions linking to cart, with state machine (information, shipping, payment, processing, completed, abandoned)

### Step 4.2: Models
Create Eloquent models with proper relationships, casts, and BelongsToStore trait where applicable:
- Cart, CartItem, Discount, DiscountRule, ShippingZone, ShippingRate, TaxZone, TaxRate, Checkout
- Create factories for each model
- Create enums: CartStatus, DiscountType, DiscountStatus, ShippingRateType, CheckoutState

### Step 4.3: Cart Service (app/Services/CartService.php)
Implement the cart service with these methods:
- `getOrCreateCart(Store $store, ?Customer $customer, ?string $sessionId): Cart`
- `addItem(Cart $cart, ProductVariant $variant, int $quantity): CartItem` - with inventory availability check
- `updateItemQuantity(CartItem $item, int $quantity): CartItem` - with optimistic locking via cart version
- `removeItem(CartItem $item): void`
- `applyDiscountCode(Cart $cart, string $code): Cart` - validates code, checks rules
- `removeDiscount(Cart $cart): Cart`
- `mergeGuestCart(Cart $guestCart, Customer $customer): Cart` - merge on login
- `recalculateTotals(Cart $cart): Cart` - subtotal, discount, tax, shipping, total
- Use optimistic concurrency: increment cart.version on every mutation, throw if stale

### Step 4.4: Discount Service (app/Services/DiscountService.php)
- `validateCode(Store $store, string $code): Discount` - check active, date range, usage limits
- `calculateDiscount(Discount $discount, Cart $cart): int` - apply percentage or fixed
- `checkRules(Discount $discount, Cart $cart): bool` - evaluate discount_rules JSON
- `incrementUsage(Discount $discount): void`

### Step 4.5: Shipping Calculator (app/Services/ShippingCalculator.php)
- `getAvailableRates(Cart $cart, array $shippingAddress): Collection` - match zones by country/region
- `calculateRate(ShippingRate $rate, Cart $cart): int` - flat, weight-based, or price-based

### Step 4.6: Tax Calculator (app/Services/TaxCalculator.php)
- `calculateTax(Cart $cart, array $address): int` - match zones, apply rates
- `getTaxBreakdown(Cart $cart, array $address): array` - return per-rate breakdown
- Support tax-inclusive and tax-exclusive pricing

### Step 4.7: Pricing Engine (app/Services/PricingEngine.php)
Orchestrates the full price calculation:
- Takes a Cart, applies discount, calculates shipping, calculates tax
- Returns a PriceBreakdown value object with subtotal, discount_amount, shipping_amount, tax_amount, total
- All amounts in INTEGER minor units (cents)

### Step 4.8: Checkout State Machine (app/Services/CheckoutService.php)
Implement checkout flow with state transitions:
- States: information -> shipping -> payment -> processing -> completed (also abandoned)
- `createCheckout(Cart $cart): Checkout`
- `updateInformation(Checkout $checkout, array $data): Checkout` - email, shipping/billing address
- `selectShippingRate(Checkout $checkout, ShippingRate $rate): Checkout`
- `transitionTo(Checkout $checkout, CheckoutState $state): Checkout` - validate allowed transitions
- `abandonCheckout(Checkout $checkout): void`
- Validate state transitions are valid (e.g. can't go from information directly to payment)

### Step 4.9: Storefront Cart/Checkout UI (Livewire components)
Update the existing storefront Livewire components:
- Update `app/Livewire/Storefront/CartDrawer.php` - slide-out cart drawer with items, quantity adjustment, discount code, totals
- Update `app/Livewire/Storefront/Cart/Show.php` - full cart page
- Create `app/Livewire/Storefront/Checkout/Information.php` - email, shipping address form
- Create `app/Livewire/Storefront/Checkout/Shipping.php` - shipping rate selection
- Create `app/Livewire/Storefront/Checkout/Payment.php` - payment method selection (mock)
- Create `app/Livewire/Storefront/Checkout/Complete.php` - order confirmation
- Add checkout routes to routes/storefront.php

## Testing Requirements
Write comprehensive Pest tests for:
- CartService: add/update/remove items, apply discount, merge carts, optimistic concurrency
- DiscountService: validation, calculation, rule checking
- ShippingCalculator: zone matching, rate calculation
- TaxCalculator: tax calculation, inclusive/exclusive
- PricingEngine: full price breakdown
- CheckoutService: state transitions, validation
- Target: at least 60 new tests

## Important Rules
- Use `php artisan make:model` commands where possible (with `--no-interaction`)
- All monetary amounts as INTEGER in minor units (cents)
- Use BelongsToStore trait on store-scoped models
- Use existing patterns from Phase 1-3 code for consistency
- Run `vendor/bin/pint --dirty --format agent` before finishing
- Run `php artisan test --compact` to verify all tests pass (both old and new)
- DO NOT modify existing test files
- DO NOT create any git commits
- Read existing models, services, and tests to understand patterns before writing code


text:
Phase 4 agent is running in the background. While it works on the Cart/Checkout/Discounts system, let me also prepare Phase 5 (Payments, Orders, Fulfillment) by reading the relevant specs so I can launch it as soon as Phase 4 completes. Let me also launch Phase 6 (Customer Accounts) and Phase 7 (Admin Panel) prep work in parallel since they're somewhat independent.

Let me check on Phase 4 progress periodically while reading the specs needed for Phase 5.


content:
     1→# 01 - Database Schema Specification
     2→
     3→> E-commerce platform - SQLite database schema for Laravel 12
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| Primary keys | Auto-incrementing integer primary key on every table (column: `id`) |
    12→| Tenant isolation | `store_id` foreign key on every tenant-scoped table |
    13→| Monetary amounts | Stored as **integer in minor units** (cents). Column names end in `_amount`. |
    14→| Booleans | Integer, not null, default 0 (0 = false, 1 = true) |
    15→| JSON data | Text column - Laravel casts to/from JSON at the application layer |
    16→| Enumerations | Check constraints on text columns |
    17→| Timestamps | Text (ISO-8601) via Laravel's `created_at` / `updated_at` |
    18→| Soft deletes | Only where explicitly noted (`deleted_at` text, nullable) |
    19→| Encrypted columns | Suffixed `_encrypted` - encrypted at application layer, stored as text |
    20→
    21→---
    22→
    23→## Epic 1 - Foundation and Multi-Store Tenancy
    24→
    25→### organizations
    26→
    27→Billing entity that owns one or more stores.
    28→
    29→| Column | Type | Nullable | Default | Constraints | Notes |
    30→|---|---|---|---|---|---|
    31→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    32→| name | TEXT | No | - | - | Organization display name |
    33→| billing_email | TEXT | No | - | - | Primary billing contact |
    34→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    35→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    36→
    37→**Indexes:**
    38→
    39→- `idx_organizations_billing_email` on (`billing_email`) - Supports lookup by billing email
    40→
    41→---
    42→
    43→### stores
    44→
    45→Individual storefront belonging to an organization.
    46→
    47→| Column | Type | Nullable | Default | Constraints | Notes |
    48→|---|---|---|---|---|---|
    49→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    50→| organization_id | INTEGER | No | - | FK -> organizations(id) ON DELETE CASCADE | Parent organization |

content:
# 01 - Database Schema Specification

> E-commerce platform - SQLite database schema for Laravel 12

---

## Conventions

| Convention | Detail |
|---|---|
| Primary keys | Auto-incrementing integer primary key on every table (column: `id`) |
| Tenant isolation | `store_id` foreign key on every tenant-scoped table |
| Monetary amounts | Stored as **integer in minor units** (cents). Column names end in `_amount`. |
| Booleans | Integer, not null, default 0 (0 = false, 1 = true) |
| JSON data | Text column - Laravel casts to/from JSON at the application layer |
| Enumerations | Check constraints on text columns |
| Timestamps | Text (ISO-8601) via Laravel's `created_at` / `updated_at` |
| Soft deletes | Only where explicitly noted (`deleted_at` text, nullable) |
| Encrypted columns | Suffixed `_encrypted` - encrypted at application layer, stored as text |

---

## Epic 1 - Foundation and Multi-Store Tenancy

### organizations

Billing entity that owns one or more stores.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| name | TEXT | No | - | - | Organization display name |
| billing_email | TEXT | No | - | - | Primary billing contact |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_organizations_billing_email` on (`billing_email`) - Supports lookup by billing email

---

### stores

Individual storefront belonging to an organization.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| organization_id | INTEGER | No | - | FK -> organizations(id) ON DELETE CASCADE | Parent organization |


content:
     1→# 05 - Business Logic Specification
     2→
     3→> E-commerce platform - All domain logic, state machines, calculations, and rules for Laravel 12
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| Monetary amounts | **INTEGER in minor units** (cents). All arithmetic uses integers - never floating point. |
    12→| Percentage storage | Basis points where noted (e.g., 1900 = 19.00%) or whole-number percentage in `discounts.value_amount` (e.g., 15 = 15%) |
    13→| Rounding | Round each line independently, then sum (prevents rounding drift) |
    14→| Database | SQLite - single-writer, WAL mode, no advisory locks needed |
    15→| Time | All timestamps in ISO-8601 UTC via Laravel `created_at` / `updated_at` |
    16→| Currency | Single currency per store (`stores.default_currency`). No multi-currency conversion. |
    17→
    18→---
    19→
    20→## 1. Multi-Tenancy and Store Isolation
    21→
    22→### 1.1 Tenant Resolution Middleware
    23→
    24→**Class:** `App\Http\Middleware\ResolveStore`
    25→
    26→**Register** in `bootstrap/app.php` under both the `storefront` and `admin` middleware groups.
    27→
    28→#### Storefront Route Resolution (from request hostname)
    29→
    30→1. Extract hostname from the incoming HTTP request.
    31→2. Look up `store_domains` table where `hostname` matches.
    32→3. Cache the hostname-to-store_id mapping using file cache with a 5-minute TTL. Cache key pattern: `store_domain:{hostname}`.
    33→4. Retrieve the full Store model and bind it as a singleton in the application container under the key `current_store`.
    34→5. If hostname is not found in `store_domains`: return HTTP 404.
    35→6. If the resolved store has `status = 'suspended'`: return HTTP 503 maintenance page.
    36→
    37→#### Admin Route Resolution (from session)
    38→
    39→1. Read the selected store ID from the authenticated user's session (key: `current_store_id`).
    40→2. Verify the authenticated user has a `store_users` record for that store.
    41→3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.
    42→
    43→```mermaid
    44→flowchart TD
    45→    A[Incoming Request] --> B{Route type?}
    46→    B -->|Storefront| C[Extract hostname]
    47→    C --> D{Hostname in cache?}
    48→    D -->|Yes| E[Load store from cache]
    49→    D -->|No| F[Query store_domains table]
    50→    F --> G{Found?}
    51→    G -->|No| H[Return 404]
    52→    G -->|Yes| I[Cache hostname->store_id, 5 min TTL]
    53→    I --> E
    54→    E --> J{Store suspended?}
    55→    J -->|Yes| K[Return 503 maintenance]
    56→    J -->|No| L[Bind store as singleton, continue]
    57→
    58→    B -->|Admin| M[Read store_id from session]
    59→    M --> N{User has store_users record?}
    60→    N -->|No| O[Return 403]
    61→    N -->|Yes| P{Store suspended?}
    62→    P -->|Yes| Q[Return 403 for mutations]
    63→    P -->|No| L
    64→```
    65→
    66→### 1.2 Store Scoping
    67→
    68→**Trait:** `App\Models\Concerns\BelongsToStore`
    69→**Global Scope:** `App\Models\Scopes\StoreScope`
    70→
    71→The trait applies a global query scope to every model that uses it. The scope adds a `WHERE store_id = <current_store_id>` clause to all queries automatically.
    72→
    73→**Behavior:**
    74→
    75→- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
    76→- All SELECT, UPDATE, DELETE queries on tenant-scoped models include the store filter.
    77→- In platform-admin contexts where cross-store access is needed, the scope can be temporarily removed.
    78→
    79→**Models that must use this trait:**
    80→products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

content:
# 05 - Business Logic Specification

> E-commerce platform - All domain logic, state machines, calculations, and rules for Laravel 12

---

## Conventions

| Convention | Detail |
|---|---|
| Monetary amounts | **INTEGER in minor units** (cents). All arithmetic uses integers - never floating point. |
| Percentage storage | Basis points where noted (e.g., 1900 = 19.00%) or whole-number percentage in `discounts.value_amount` (e.g., 15 = 15%) |
| Rounding | Round each line independently, then sum (prevents rounding drift) |
| Database | SQLite - single-writer, WAL mode, no advisory locks needed |
| Time | All timestamps in ISO-8601 UTC via Laravel `created_at` / `updated_at` |
| Currency | Single currency per store (`stores.default_currency`). No multi-currency conversion. |

---

## 1. Multi-Tenancy and Store Isolation

### 1.1 Tenant Resolution Middleware

**Class:** `App\Http\Middleware\ResolveStore`

**Register** in `bootstrap/app.php` under both the `storefront` and `admin` middleware groups.

#### Storefront Route Resolution (from request hostname)

1. Extract hostname from the incoming HTTP request.
2. Look up `store_domains` table where `hostname` matches.
3. Cache the hostname-to-store_id mapping using file cache with a 5-minute TTL. Cache key pattern: `store_domain:{hostname}`.
4. Retrieve the full Store model and bind it as a singleton in the application container under the key `current_store`.
5. If hostname is not found in `store_domains`: return HTTP 404.
6. If the resolved store has `status = 'suspended'`: return HTTP 503 maintenance page.

#### Admin Route Resolution (from session)

1. Read the selected store ID from the authenticated user's session (key: `current_store_id`).
2. Verify the authenticated user has a `store_users` record for that store.
3. If the store is suspended: return HTTP 403 Forbidden. Admin users can still view settings but cannot perform storefront-affecting mutations.

```mermaid
flowchart TD
    A[Incoming Request] --> B{Route type?}
    B -->|Storefront| C[Extract hostname]
    C --> D{Hostname in cache?}
    D -->|Yes| E[Load store from cache]
    D -->|No| F[Query store_domains table]
    F --> G{Found?}
    G -->|No| H[Return 404]
    G -->|Yes| I[Cache hostname->store_id, 5 min TTL]
    I --> E
    E --> J{Store suspended?}
    J -->|Yes| K[Return 503 maintenance]
    J -->|No| L[Bind store as singleton, continue]

    B -->|Admin| M[Read store_id from session]
    M --> N{User has store_users record?}
    N -->|No| O[Return 403]
    N -->|Yes| P{Store suspended?}
    P -->|Yes| Q[Return 403 for mutations]
    P -->|No| L
```

### 1.2 Store Scoping

**Trait:** `App\Models\Concerns\BelongsToStore`
**Global Scope:** `App\Models\Scopes\StoreScope`

The trait applies a global query scope to every model that uses it. The scope adds a `WHERE store_id = <current_store_id>` clause to all queries automatically.

**Behavior:**

- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
- All SELECT, UPDATE, DELETE queries on tenant-scoped models include the store filter.
- In platform-admin contexts where cross-store access is needed, the scope can be temporarily removed.

**Models that must use this trait:**
products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.


thinking: Let me read more of the database schema for Phase 5 (payments, orders, fulfillment) while Phase 4 agent works.


text: Let me read the Phase 5 portions of the schema (payments, orders, fulfillment) to prepare the next agent launch.


content:
751:### orders
818:### payments
846:### refunds
869:### fulfillments

mode: content


content: No matches found


content:
   751→### orders
   752→
   753→Placed orders with financial and fulfillment tracking.
   754→
   755→| Column | Type | Nullable | Default | Constraints | Notes |
   756→|---|---|---|---|---|---|
   757→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   758→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
   759→| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for guest orders |
   760→| order_number | TEXT | No | - | UNIQUE per (store_id, order_number) | Human-readable order number |
   761→| payment_method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used for this order |
   762→| status | TEXT | No | 'pending' | CHECK IN ('pending', 'paid', 'fulfilled', 'cancelled', 'refunded') | Overall order status |
   763→| financial_status | TEXT | No | 'pending' | CHECK IN ('pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided') | Payment status |
   764→| fulfillment_status | TEXT | No | 'unfulfilled' | CHECK IN ('unfulfilled', 'partial', 'fulfilled') | Shipping status |
   765→| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
   766→| subtotal_amount | INTEGER | No | 0 | - | Sum of line items in minor units |
   767→| discount_amount | INTEGER | No | 0 | - | Total discount in minor units |
   768→| shipping_amount | INTEGER | No | 0 | - | Shipping cost in minor units |
   769→| tax_amount | INTEGER | No | 0 | - | Total tax in minor units |
   770→| total_amount | INTEGER | No | 0 | - | Grand total in minor units |
   771→| email | TEXT | Yes | NULL | - | Contact email at time of order |
   772→| billing_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
   773→| shipping_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
   774→| placed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of placement |
   775→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   776→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   777→
   778→**Indexes:**
   779→
   780→- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
   781→- `idx_orders_store_id` on (`store_id`) - FK lookup
   782→- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
   783→- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
   784→- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
   785→- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
   786→- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries
   787→
   788→---
   789→
   790→### order_lines
   791→
   792→Individual items within an order. Contains snapshots of product data at time of purchase.
   793→
   794→| Column | Type | Nullable | Default | Constraints | Notes |
   795→|---|---|---|---|---|---|
   796→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   797→| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
   798→| product_id | INTEGER | Yes | NULL | FK -> products(id) ON DELETE SET NULL | Nullable: survives product deletion |
   799→| variant_id | INTEGER | Yes | NULL | FK -> product_variants(id) ON DELETE SET NULL | Nullable: survives variant deletion |
   800→| title_snapshot | TEXT | No | - | - | Product title at time of purchase |
   801→| sku_snapshot | TEXT | Yes | NULL | - | SKU at time of purchase |
   802→| quantity | INTEGER | No | 1 | - | Number of units |
   803→| unit_price_amount | INTEGER | No | 0 | - | Price per unit in minor units |
   804→| total_amount | INTEGER | No | 0 | - | Line total in minor units |
   805→| tax_lines_json | TEXT | No | '[]' | - | JSON array of tax line items |
   806→| discount_allocations_json | TEXT | No | '[]' | - | JSON array of discount allocations |
   807→
   808→**Indexes:**
   809→
   810→- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
   811→- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
   812→- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant
   813→
   814→**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.
   815→
   816→---
   817→
   818→### payments
   819→
   820→Payment attempts and captures for an order.
   821→
   822→| Column | Type | Nullable | Default | Constraints | Notes |
   823→|---|---|---|---|---|---|
   824→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   825→| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
   826→| provider | TEXT | No | 'mock' | CHECK IN ('mock') | Payment provider (mock PSP only) |
   827→| method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used |
   828→| provider_payment_id | TEXT | Yes | NULL | - | Mock payment reference ID |
   829→| status | TEXT | No | 'pending' | CHECK IN ('pending', 'captured', 'failed', 'refunded') | |
   830→| amount | INTEGER | No | 0 | - | Payment amount in minor units |
   831→| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
   832→| raw_json_encrypted | TEXT | Yes | NULL | - | Encrypted provider response (PCI compliance) |
   833→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   834→
   835→**Indexes:**
   836→
   837→- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
   838→- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
   839→- `idx_payments_method` on (`method`) - Filtering payments by method
   840→- `idx_payments_status` on (`status`) - Filtering payments by status
   841→
   842→**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).
   843→
   844→---
   845→
   846→### refunds
   847→
   848→Refund records linked to an order and payment.
   849→
   850→| Column | Type | Nullable | Default | Constraints | Notes |
   851→|---|---|---|---|---|---|
   852→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   853→| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
   854→| payment_id | INTEGER | No | - | FK -> payments(id) ON DELETE CASCADE | Source payment |
   855→| amount | INTEGER | No | 0 | - | Refund amount in minor units |
   856→| reason | TEXT | Yes | NULL | - | Refund reason |
   857→| status | TEXT | No | 'pending' | CHECK IN ('pending', 'processed', 'failed') | |
   858→| provider_refund_id | TEXT | Yes | NULL | - | External refund ID |
   859→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   860→
   861→**Indexes:**
   862→
   863→- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
   864→- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
   865→- `idx_refunds_status` on (`status`) - Filtering refunds by status
   866→
   867→---
   868→
   869→### fulfillments
   870→
   871→Shipment tracking for order fulfillment.
   872→
   873→| Column | Type | Nullable | Default | Constraints | Notes |
   874→|---|---|---|---|---|---|
   875→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   876→| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
   877→| status | TEXT | No | 'pending' | CHECK IN ('pending', 'shipped', 'delivered') | |
   878→| tracking_company | TEXT | Yes | NULL | - | Carrier name |
   879→| tracking_number | TEXT | Yes | NULL | - | Tracking number |
   880→| tracking_url | TEXT | Yes | NULL | - | Tracking page URL |
   881→| shipped_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   882→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   883→
   884→**Indexes:**
   885→
   886→- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
   887→- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
   888→- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup
   889→
   890→---
   891→
   892→### fulfillment_lines
   893→
   894→Links specific order lines and quantities to a fulfillment.
   895→
   896→| Column | Type | Nullable | Default | Constraints | Notes |
   897→|---|---|---|---|---|---|
   898→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   899→| fulfillment_id | INTEGER | No | - | FK -> fulfillments(id) ON DELETE CASCADE | Parent fulfillment |
   900→| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
   901→| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |
   902→
   903→**Indexes:**
   904→
   905→- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
   906→- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment
   907→
   908→**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.
   909→
   910→---
   911→
   912→## Epic 7 - Analytics
   913→
   914→### analytics_events
   915→
   916→Raw event stream for storefront analytics.
   917→
   918→| Column | Type | Nullable | Default | Constraints | Notes |
   919→|---|---|---|---|---|---|
   920→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   921→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
   922→| type | TEXT | No | - | - | Event type (see notes) |
   923→| session_id | TEXT | Yes | NULL | - | Browser session identifier |
   924→| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
   925→| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
   926→| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
   927→| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
   928→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   929→
   930→**Indexes:**
   931→
   932→- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
   933→- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
   934→- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
   935→- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
   936→- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
   937→- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication
   938→
   939→**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.
   940→
   941→---
   942→
   943→### analytics_daily
   944→
   945→Pre-aggregated daily metrics per store. Composite primary key.
   946→
   947→| Column | Type | Nullable | Default | Constraints | Notes |
   948→|---|---|---|---|---|---|
   949→| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
   950→| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |

content:
### orders

Placed orders with financial and fulfillment tracking.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for guest orders |
| order_number | TEXT | No | - | UNIQUE per (store_id, order_number) | Human-readable order number |
| payment_method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used for this order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'paid', 'fulfilled', 'cancelled', 'refunded') | Overall order status |
| financial_status | TEXT | No | 'pending' | CHECK IN ('pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided') | Payment status |
| fulfillment_status | TEXT | No | 'unfulfilled' | CHECK IN ('unfulfilled', 'partial', 'fulfilled') | Shipping status |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| subtotal_amount | INTEGER | No | 0 | - | Sum of line items in minor units |
| discount_amount | INTEGER | No | 0 | - | Total discount in minor units |
| shipping_amount | INTEGER | No | 0 | - | Shipping cost in minor units |
| tax_amount | INTEGER | No | 0 | - | Total tax in minor units |
| total_amount | INTEGER | No | 0 | - | Grand total in minor units |
| email | TEXT | Yes | NULL | - | Contact email at time of order |
| billing_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| shipping_address_json | TEXT | Yes | NULL | - | JSON address snapshot |
| placed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of placement |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_orders_store_order_number` UNIQUE on (`store_id`, `order_number`) - Unique order number per store
- `idx_orders_store_id` on (`store_id`) - FK lookup
- `idx_orders_customer_id` on (`customer_id`) - Listing orders per customer
- `idx_orders_store_status` on (`store_id`, `status`) - Filtering by overall status
- `idx_orders_store_financial` on (`store_id`, `financial_status`) - Filtering by payment status
- `idx_orders_store_fulfillment` on (`store_id`, `fulfillment_status`) - Filtering by shipping status
- `idx_orders_placed_at` on (`store_id`, `placed_at`) - Date-range order queries

---

### order_lines

Individual items within an order. Contains snapshots of product data at time of purchase.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| product_id | INTEGER | Yes | NULL | FK -> products(id) ON DELETE SET NULL | Nullable: survives product deletion |
| variant_id | INTEGER | Yes | NULL | FK -> product_variants(id) ON DELETE SET NULL | Nullable: survives variant deletion |
| title_snapshot | TEXT | No | - | - | Product title at time of purchase |
| sku_snapshot | TEXT | Yes | NULL | - | SKU at time of purchase |
| quantity | INTEGER | No | 1 | - | Number of units |
| unit_price_amount | INTEGER | No | 0 | - | Price per unit in minor units |
| total_amount | INTEGER | No | 0 | - | Line total in minor units |
| tax_lines_json | TEXT | No | '[]' | - | JSON array of tax line items |
| discount_allocations_json | TEXT | No | '[]' | - | JSON array of discount allocations |

**Indexes:**

- `idx_order_lines_order_id` on (`order_id`) - FK lookup, listing lines per order
- `idx_order_lines_product_id` on (`product_id`) - Finding order lines by product
- `idx_order_lines_variant_id` on (`variant_id`) - Finding order lines by variant

**Notes:** `product_id` and `variant_id` are nullable with SET NULL on delete so that order history survives product deletion. `tax_lines_json` example: `[{"title": "State Tax", "rate": 800, "amount": 400}]` (rate in basis points, e.g. 800 = 8%). `discount_allocations_json` example: `[{"discount_id": 5, "amount": 250}]`.

---

### payments

Payment attempts and captures for an order.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| provider | TEXT | No | 'mock' | CHECK IN ('mock') | Payment provider (mock PSP only) |
| method | TEXT | No | - | CHECK IN ('credit_card', 'paypal', 'bank_transfer') | Payment method used |
| provider_payment_id | TEXT | Yes | NULL | - | Mock payment reference ID |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'captured', 'failed', 'refunded') | |
| amount | INTEGER | No | 0 | - | Payment amount in minor units |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| raw_json_encrypted | TEXT | Yes | NULL | - | Encrypted provider response (PCI compliance) |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_payments_order_id` on (`order_id`) - FK lookup, listing payments per order
- `idx_payments_provider_id` on (`provider`, `provider_payment_id`) - Payment reconciliation by provider ID
- `idx_payments_method` on (`method`) - Filtering payments by method
- `idx_payments_status` on (`status`) - Filtering payments by status

**Notes:** `raw_json_encrypted` stores the mock payment provider response, encrypted at the application layer. `amount` is in minor units (cents). The `provider` is always `mock` since the platform uses a mock PSP (no real payment gateway integration). The `method` column records which payment method the customer selected (credit_card, paypal, or bank_transfer).

---

### refunds

Refund records linked to an order and payment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| payment_id | INTEGER | No | - | FK -> payments(id) ON DELETE CASCADE | Source payment |
| amount | INTEGER | No | 0 | - | Refund amount in minor units |
| reason | TEXT | Yes | NULL | - | Refund reason |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'processed', 'failed') | |
| provider_refund_id | TEXT | Yes | NULL | - | External refund ID |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_refunds_order_id` on (`order_id`) - FK lookup, listing refunds per order
- `idx_refunds_payment_id` on (`payment_id`) - Finding refunds per payment
- `idx_refunds_status` on (`status`) - Filtering refunds by status

---

### fulfillments

Shipment tracking for order fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| order_id | INTEGER | No | - | FK -> orders(id) ON DELETE CASCADE | Parent order |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'shipped', 'delivered') | |
| tracking_company | TEXT | Yes | NULL | - | Carrier name |
| tracking_number | TEXT | Yes | NULL | - | Tracking number |
| tracking_url | TEXT | Yes | NULL | - | Tracking page URL |
| shipped_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_fulfillments_order_id` on (`order_id`) - FK lookup, listing fulfillments per order
- `idx_fulfillments_status` on (`status`) - Filtering fulfillments by status
- `idx_fulfillments_tracking` on (`tracking_company`, `tracking_number`) - Tracking number lookup

---

### fulfillment_lines

Links specific order lines and quantities to a fulfillment.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| fulfillment_id | INTEGER | No | - | FK -> fulfillments(id) ON DELETE CASCADE | Parent fulfillment |
| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |

**Indexes:**

- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment

**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.

---

## Epic 7 - Analytics

### analytics_events

Raw event stream for storefront analytics.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| type | TEXT | No | - | - | Event type (see notes) |
| session_id | TEXT | Yes | NULL | - | Browser session identifier |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication

**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.

---

### analytics_daily

Pre-aggregated daily metrics per store. Composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |


content:
     1→# 09 - Implementation Roadmap and Pest Test Specification
     2→
     3→> E-commerce platform - Step-by-step build order and exhaustive test plan for Laravel 12 + Livewire v4 + Flux UI Free + SQLite + Tailwind CSS v4
     4→
     5→---
     6→
     7→## 1. Technology Decisions (Final)
     8→
     9→These decisions adapt the original PDF spec for a self-contained, benchmarkable Laravel application. All decisions are final and must not be revisited.
    10→
    11→| Concern | PDF Default | Our Decision | Rationale |
    12→|---------|-------------|--------------|-----------|
    13→| Database | PostgreSQL | SQLite | Self-contained, no external DB, zero config |
    14→| Cache | Redis | File (Array for tests) | No Redis dependency |
    15→| Session | Redis | File | No Redis dependency |
    16→| Queue | Redis + Horizon | Sync (dev) / Database (prod) | No Redis dependency, sync for immediate feedback |
    17→| Search | Elasticsearch + Scout | SQLite FTS5 | Self-contained, no external service |
    18→| Storage | S3 | Local filesystem (storage/app/public) | No AWS dependency |
    19→| Admin UI | React/Vue SPA | Livewire v4 + Flux UI Free | Server-rendered, matches project stack |
    20→| Storefront | Blade SSR + Vite | Blade + Livewire + Tailwind v4 | Server-rendered, matches project stack |
    21→| Auth (admin) | Sanctum SPA | Laravel session auth | Standard Livewire auth pattern |
    22→| Auth (API) | Sanctum tokens | Sanctum personal access tokens | For programmatic API access |
    23→| Auth (customer) | Separate guard | Custom `customer` guard | Storefront customer auth |
    24→| OAuth (apps) | Passport | Deferred/Stubbed | Simplifies initial build, can add later |
    25→| Payments | Stripe + PayPal | Mock PSP (in-process) | No external API calls. MockPaymentProvider handles credit card, PayPal, bank transfer with magic card numbers for testing. |
    26→| Email | SMTP/SES | Log driver (dev) / SMTP | Log for benchmark, SMTP configurable |
    27→| Monitoring | OpenTelemetry | Laravel Log (structured JSON) | Simplified for benchmark |
    28→| CI/CD | GitHub Actions | Not implemented | Out of scope for one-shot benchmark |
    29→| Docker | Multi-stage | Not implemented | App served by Laravel Herd locally |
    30→
    31→---
    32→
    33→## 2. Implementation Phases (Build Order)
    34→
    35→Each phase builds on the previous. A coding agent should execute these in strict sequential order. Tests are written alongside each phase (see Section 3 for the full test specification).
    36→
    37→### Phase Dependency Graph
    38→
    39→```mermaid
    40→flowchart TD
    41→    P1[Phase 1: Foundation]
    42→    P2[Phase 2: Catalog]
    43→    P3[Phase 3: Themes and Storefront Layout]
    44→    P4[Phase 4: Cart, Checkout, Discounts, Shipping, Taxes]
    45→    P5[Phase 5: Payments, Orders, Fulfillment]
    46→    P6[Phase 6: Customer Accounts]
    47→    P7[Phase 7: Admin Panel]
    48→    P8[Phase 8: Search]
    49→    P9[Phase 9: Analytics]
    50→    P10[Phase 10: Apps and Webhooks]
    51→    P11[Phase 11: Polish]
    52→    P12[Phase 12: Full Test Suite]
    53→
    54→    P1 --> P2
    55→    P1 --> P3
    56→    P2 --> P4
    57→    P3 --> P4
    58→    P4 --> P5
    59→    P5 --> P6
    60→    P5 --> P7
    61→    P2 --> P8
    62→    P5 --> P9
    63→    P5 --> P10
    64→    P6 --> P11
    65→    P7 --> P11
    66→    P8 --> P11
    67→    P9 --> P11
    68→    P10 --> P11
    69→    P11 --> P12
    70→```
    71→
    72→---
    73→
    74→### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
    75→
    76→**Priority:** CRITICAL - everything depends on this.
    77→
    78→#### Step 1.1: Environment and Config
    79→
    80→| Config File | Setting | Value | Purpose |
    81→|-------------|---------|-------|---------|
    82→| `.env` | `DB_CONNECTION` | `sqlite` | Use SQLite as database |
    83→| `.env` | `DB_DATABASE` | `database/database.sqlite` | SQLite database file path |
    84→| `.env` | `CACHE_STORE` | `file` | File-based caching |
    85→| `.env` | `SESSION_DRIVER` | `file` | File-based sessions |
    86→| `.env` | `QUEUE_CONNECTION` | `sync` | Synchronous queue for dev |
    87→| `.env` | `MAIL_MAILER` | `log` | Log-based mail for dev |
    88→| `.env` | `FILESYSTEM_DISK` | `local` | Local filesystem storage |
    89→| `config/database.php` | SQLite connection | WAL mode, foreign keys enabled, busy_timeout=5000, synchronous=normal | Optimized SQLite configuration |
    90→| `config/session.php` | `driver` | `file` | Session driver |
    91→| `config/session.php` | `lifetime` | `120` | Session lifetime in minutes |
    92→| `config/cache.php` | `default` | `file` | Cache driver |
    93→| `config/queue.php` | `default` | `sync` | Queue driver |
    94→| `config/auth.php` | Guards | Add `customer` guard with `customers` provider | Customer storefront authentication |
    95→| `config/auth.php` | Providers | Add `customers` provider pointing to Customer model | Customer user provider |
    96→| `config/auth.php` | Passwords | Add `customers` password broker | Customer password reset |
    97→| `config/filesystems.php` | `public` disk | Ensure configured for local storage | Media file storage |
    98→| `config/logging.php` | Channels | Add structured JSON channel | Structured logging |
    99→
   100→#### Step 1.2: Core Migrations (Batch 1-2)

content:
# 09 - Implementation Roadmap and Pest Test Specification

> E-commerce platform - Step-by-step build order and exhaustive test plan for Laravel 12 + Livewire v4 + Flux UI Free + SQLite + Tailwind CSS v4

---

## 1. Technology Decisions (Final)

These decisions adapt the original PDF spec for a self-contained, benchmarkable Laravel application. All decisions are final and must not be revisited.

| Concern | PDF Default | Our Decision | Rationale |
|---------|-------------|--------------|-----------|
| Database | PostgreSQL | SQLite | Self-contained, no external DB, zero config |
| Cache | Redis | File (Array for tests) | No Redis dependency |
| Session | Redis | File | No Redis dependency |
| Queue | Redis + Horizon | Sync (dev) / Database (prod) | No Redis dependency, sync for immediate feedback |
| Search | Elasticsearch + Scout | SQLite FTS5 | Self-contained, no external service |
| Storage | S3 | Local filesystem (storage/app/public) | No AWS dependency |
| Admin UI | React/Vue SPA | Livewire v4 + Flux UI Free | Server-rendered, matches project stack |
| Storefront | Blade SSR + Vite | Blade + Livewire + Tailwind v4 | Server-rendered, matches project stack |
| Auth (admin) | Sanctum SPA | Laravel session auth | Standard Livewire auth pattern |
| Auth (API) | Sanctum tokens | Sanctum personal access tokens | For programmatic API access |
| Auth (customer) | Separate guard | Custom `customer` guard | Storefront customer auth |
| OAuth (apps) | Passport | Deferred/Stubbed | Simplifies initial build, can add later |
| Payments | Stripe + PayPal | Mock PSP (in-process) | No external API calls. MockPaymentProvider handles credit card, PayPal, bank transfer with magic card numbers for testing. |
| Email | SMTP/SES | Log driver (dev) / SMTP | Log for benchmark, SMTP configurable |
| Monitoring | OpenTelemetry | Laravel Log (structured JSON) | Simplified for benchmark |
| CI/CD | GitHub Actions | Not implemented | Out of scope for one-shot benchmark |
| Docker | Multi-stage | Not implemented | App served by Laravel Herd locally |

---

## 2. Implementation Phases (Build Order)

Each phase builds on the previous. A coding agent should execute these in strict sequential order. Tests are written alongside each phase (see Section 3 for the full test specification).

### Phase Dependency Graph

```mermaid
flowchart TD
    P1[Phase 1: Foundation]
    P2[Phase 2: Catalog]
    P3[Phase 3: Themes and Storefront Layout]
    P4[Phase 4: Cart, Checkout, Discounts, Shipping, Taxes]
    P5[Phase 5: Payments, Orders, Fulfillment]
    P6[Phase 6: Customer Accounts]
    P7[Phase 7: Admin Panel]
    P8[Phase 8: Search]
    P9[Phase 9: Analytics]
    P10[Phase 10: Apps and Webhooks]
    P11[Phase 11: Polish]
    P12[Phase 12: Full Test Suite]

    P1 --> P2
    P1 --> P3
    P2 --> P4
    P3 --> P4
    P4 --> P5
    P5 --> P6
    P5 --> P7
    P2 --> P8
    P5 --> P9
    P5 --> P10
    P6 --> P11
    P7 --> P11
    P8 --> P11
    P9 --> P11
    P10 --> P11
    P11 --> P12
```

---

### Phase 1: Foundation (Migrations, Models, Middleware, Auth)

**Priority:** CRITICAL - everything depends on this.

#### Step 1.1: Environment and Config

| Config File | Setting | Value | Purpose |
|-------------|---------|-------|---------|
| `.env` | `DB_CONNECTION` | `sqlite` | Use SQLite as database |
| `.env` | `DB_DATABASE` | `database/database.sqlite` | SQLite database file path |
| `.env` | `CACHE_STORE` | `file` | File-based caching |
| `.env` | `SESSION_DRIVER` | `file` | File-based sessions |
| `.env` | `QUEUE_CONNECTION` | `sync` | Synchronous queue for dev |
| `.env` | `MAIL_MAILER` | `log` | Log-based mail for dev |
| `.env` | `FILESYSTEM_DISK` | `local` | Local filesystem storage |
| `config/database.php` | SQLite connection | WAL mode, foreign keys enabled, busy_timeout=5000, synchronous=normal | Optimized SQLite configuration |
| `config/session.php` | `driver` | `file` | Session driver |
| `config/session.php` | `lifetime` | `120` | Session lifetime in minutes |
| `config/cache.php` | `default` | `file` | Cache driver |
| `config/queue.php` | `default` | `sync` | Queue driver |
| `config/auth.php` | Guards | Add `customer` guard with `customers` provider | Customer storefront authentication |
| `config/auth.php` | Providers | Add `customers` provider pointing to Customer model | Customer user provider |
| `config/auth.php` | Passwords | Add `customers` password broker | Customer password reset |
| `config/filesystems.php` | `public` disk | Ensure configured for local storage | Media file storage |
| `config/logging.php` | Channels | Add structured JSON channel | Structured logging |

#### Step 1.2: Core Migrations (Batch 1-2)


thinking: Good - I now have a good understanding of the Phase 5 schema. Let me also read the business logic for Phase 5 (payments, orders, fulfillment) while the Phase 4 agent works. Let me also read the roadmap for Phase 5.


text: Let me read the Phase 5 business logic and roadmap sections.


content: 599:### Phase 5: Payments, Orders, Fulfillment


content:
   599→### Phase 5: Payments, Orders, Fulfillment
   600→
   601→**Priority:** HIGH - completes the purchase flow.
   602→
   603→#### Step 5.1: Migrations (Batch 5-7)
   604→
   605→1. `create_customers_table` - FK: `store_id`
   606→2. `create_customer_addresses_table` - FK: `customer_id`
   607→3. `create_orders_table` - FK: `store_id`, `customer_id`
   608→4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
   609→5. `create_payments_table` - FK: `order_id`
   610→6. `create_refunds_table` - FK: `order_id`, `payment_id`
   611→7. `create_fulfillments_table` - FK: `order_id`
   612→8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`
   613→
   614→#### Step 5.2: Models
   615→
   616→**Files to create:**
   617→
   618→| Type | Name | Purpose |
   619→|------|------|---------|
   620→| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
   621→| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
   622→| Model (with migration, factory, seeder) | `Order` | Order entity |
   623→| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
   624→| Model (with migration, factory, seeder) | `Payment` | Payment record |
   625→| Model (with migration, factory, seeder) | `Refund` | Refund record |
   626→| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
   627→| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |
   628→
   629→**Relationship definitions:**
   630→
   631→| Model | Key Relationships |
   632→|-------|-------------------|
   633→| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
   634→| `CustomerAddress` | `belongsTo(Customer)` |
   635→| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
   636→| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
   637→| `Payment` | `belongsTo(Order)` |
   638→| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
   639→| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
   640→| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |
   641→
   642→**Enums to create:**
   643→
   644→| Enum | Values |
   645→|------|--------|
   646→| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
   647→| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
   648→| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
   649→| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
   650→| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
   651→| `RefundStatus` | `Pending`, `Processed`, `Failed` |
   652→| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |
   653→
   654→#### Step 5.3: Payment Service (Mock PSP)
   655→
   656→**Files to create:**
   657→
   658→| Type | Name | Purpose |
   659→|------|------|---------|
   660→| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
   661→| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
   662→| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |
   663→
   664→**PaymentProvider interface methods:**
   665→
   666→| Method | Description |
   667→|--------|-------------|
   668→| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
   669→| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |
   670→
   671→**MockPaymentProvider requirements:**
   672→- No external API calls, no webhooks, no redirects
   673→- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
   674→- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
   675→- Bank transfer: deferred capture, returns PaymentResult with status=pending
   676→- Generates mock reference IDs (`mock_` + random string)
   677→
   678→**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
   679→
   680→#### Step 5.3b: Admin Payment Confirmation
   681→
   682→**Files to create:**
   683→
   684→| Type | Name | Purpose |
   685→|------|------|---------|
   686→| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |
   687→
   688→**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
   689→1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
   690→2. Update `financial_status` to `paid`, `status` to `paid`
   691→3. Update payment record status to `captured`
   692→4. Commit reserved inventory
   693→5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
   694→6. Dispatch `OrderPaid` event
   695→
   696→#### Step 5.4: Order Service
   697→
   698→**File to create:** `App\Services\OrderService`
   699→
   700→| Method | Description |
   701→|--------|-------------|
   702→| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
   703→| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
   704→| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |
   705→
   706→Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.
   707→
   708→#### Step 5.5: Refund Service
   709→
   710→**File to create:** `App\Services\RefundService`
   711→
   712→| Method | Description |
   713→|--------|-------------|
   714→| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |
   715→
   716→- Partial refunds: `financial_status` -> `partially_refunded`
   717→- Full refunds: `financial_status` -> `refunded`
   718→- Restock flag: calls `InventoryService::restock()` for each line

content:
### Phase 5: Payments, Orders, Fulfillment

**Priority:** HIGH - completes the purchase flow.

#### Step 5.1: Migrations (Batch 5-7)

1. `create_customers_table` - FK: `store_id`
2. `create_customer_addresses_table` - FK: `customer_id`
3. `create_orders_table` - FK: `store_id`, `customer_id`
4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
5. `create_payments_table` - FK: `order_id`
6. `create_refunds_table` - FK: `order_id`, `payment_id`
7. `create_fulfillments_table` - FK: `order_id`
8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`

#### Step 5.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
| Model (with migration, factory, seeder) | `Order` | Order entity |
| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
| Model (with migration, factory, seeder) | `Payment` | Payment record |
| Model (with migration, factory, seeder) | `Refund` | Refund record |
| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
| `CustomerAddress` | `belongsTo(Customer)` |
| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
| `Payment` | `belongsTo(Order)` |
| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
| `RefundStatus` | `Pending`, `Processed`, `Failed` |
| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |

#### Step 5.3: Payment Service (Mock PSP)

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |

**PaymentProvider interface methods:**

| Method | Description |
|--------|-------------|
| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |

**MockPaymentProvider requirements:**
- No external API calls, no webhooks, no redirects
- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
- Bank transfer: deferred capture, returns PaymentResult with status=pending
- Generates mock reference IDs (`mock_` + random string)

**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.

#### Step 5.3b: Admin Payment Confirmation

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |

**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
2. Update `financial_status` to `paid`, `status` to `paid`
3. Update payment record status to `captured`
4. Commit reserved inventory
5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
6. Dispatch `OrderPaid` event

#### Step 5.4: Order Service

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

| Method | Description |
|--------|-------------|
| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |

Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.

#### Step 5.5: Refund Service

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

| Method | Description |
|--------|-------------|
| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |

- Partial refunds: `financial_status` -> `partially_refunded`
- Full refunds: `financial_status` -> `refunded`
- Restock flag: calls `InventoryService::restock()` for each line