<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/clear</command-name>
            <command-message>clear</command-message>
            <command-args></command-args>


<local-command-stdout></local-command-stdout>


Your mission is to implement an entire shop system based on the specifications im specs/*. You must do in one go without stopping. You must use team mode! You must test everything via Pest (unit, and functional tests). You must also additional simulate user behaviour using the Playwright MPC and confirm that all acceptance criterias are met. If you find bugs, you must fix them. The result is a perfect shop system. All requirements are perfectly implemented. All acceptance criterias are met, tested and confirmed by you.

Continuously keep track of the progress in specs/progress.md Commit your progress after every relevant iteration with a meaningful message.

When implementation is fully done, then make a full review meeting and showcase all features (customer- and admin-side) to me. In case bugs appear, you must fix them all and restart the review meeting.

You are writing production PHP (Laravel) code.

STRICT CONSTRAINTS:

1. PHPStan Compliance
- Must pass PHPStan at max level.
- No mixed.
- Explicit return types everywhere.
- Fully typed properties.
- No dynamic properties.
- No suppressed errors.
- No relying on docblocks to hide real type problems.

2. Deptrac Compliance
- Respect architectural layers.
- No cross-layer violations.
- No circular dependencies.
- If a dependency is required, introduce an interface in the correct layer.
- Do not modify architecture unless explicitly instructed.

3. Pest Testing (Mandatory)
- Every feature must include automated tests.
- Include both unit and integration tests when appropriate.
- Cover success path and failure paths.
- Cover edge cases.
- Tests must be deterministic.
- Tests must validate behavior, not implementation details.

4. QA Self-Verification (Mandatory)
   Before finalizing:
- List each acceptance criterion.
- Explicitly confirm how it is implemented.
- Explicitly confirm which test covers it.
- Validate edge cases.
- Validate negative paths.
- Ensure no undefined behavior exists.

5. Fresh Agent Review (Mandatory)

After implementation:

Step 1: A NEW agent instance must review the code.
The reviewer must:
- Ignore prior reasoning.
- Re-evaluate architecture.
- Re-evaluate PHPStan compliance.
- Re-evaluate Deptrac boundaries.
- Re-evaluate test coverage.
- Act as a strict senior reviewer.

Step 2: The reviewer must:
- Identify weaknesses.
- Identify overengineering.
- Identify missing edge cases.
- Identify architectural drift.
- Suggest concrete improvements.

Step 3:
If issues are found:
- Fix them.
- Run review again with another fresh agent.
- Repeat until no critical issues remain.

No feature is complete without independent review.


This is a massive undertaking - implementing an entire e-commerce shop system based on specifications. Let me start by understanding the full scope by reading the specs, then create a team to parallelize the work.

Let me first read the implementation roadmap and all specs to understand the full picture.


I'll start by reading all the specifications to understand the full scope, then create a team to implement this efficiently.


Error: File content (32506 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.


<tool_use_error>Sibling tool call errored</tool_use_error>


     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)
   101→
   102→Create in this dependency order:
   103→
   104→1. `create_organizations_table` - no dependencies
   105→2. `create_stores_table` - FK: `organization_id`
   106→3. `create_store_domains_table` - FK: `store_id`
   107→4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
   108→5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
   109→6. `create_store_settings_table` - PK: `store_id`
   110→
   111→All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.
   112→
   113→#### Step 1.3: Core Models
   114→
   115→**Files to create:**
   116→
   117→| Type | Name | Purpose |
   118→|------|------|---------|
   119→| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
   120→| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
   121→| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
   122→| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
   123→| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |
   124→
   125→**Relationship definitions:**
   126→
   127→| Model | Key Relationships |
   128→|-------|-------------------|
   129→| `Organization` | `hasMany(Store::class)` |
   130→| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
   131→| `StoreDomain` | `belongsTo(Store::class)` |
   132→| `User` | `belongsToMany(Store::class)` through `store_users` |
   133→| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
   134→| `StoreSettings` | `belongsTo(Store::class)` |
   135→
   136→Each model must have:
   137→- Explicit return type declarations on all methods
   138→- Proper `$fillable` or `$guarded` arrays
   139→- `casts()` method for JSON/enum columns
   140→- Factory with meaningful default values
   141→- Seeder that creates sample data
   142→
   143→#### Step 1.4: Enums
   144→
   145→**Files to create in `app/Enums/`:**
   146→
   147→| Enum | Values |
   148→|------|--------|
   149→| `StoreStatus` | `Active`, `Suspended` |
   150→| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
   151→| `StoreDomainType` | `Storefront`, `Admin`, `Api` |
   152→
   153→#### Step 1.5: Tenant Resolution Middleware
   154→
   155→**File to create:** `App\Http\Middleware\ResolveStore`
   156→
   157→Requirements:
   158→1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
   159→2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
   160→3. Return 404 if hostname not found
   161→4. Return 503 for suspended stores on storefront routes
   162→
   163→**Registration in `bootstrap/app.php`:**
   164→- Register middleware group `storefront` containing `ResolveStore`
   165→- Register middleware group `admin` containing `ResolveStore`
   166→
   167→#### Step 1.6: BelongsToStore Trait and Global Scope
   168→
   169→**Files to create:**
   170→
   171→| Type | Name | Purpose |
   172→|------|------|---------|
   173→| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
   174→| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |
   175→
   176→Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
   177→
   178→#### Step 1.7: Authentication
   179→
   180→**Admin auth requirements:**
   181→
   182→| Component | Description |
   183→|-----------|-------------|
   184→| Livewire component `Admin\Auth\Login` | Admin login form |
   185→| Livewire component `Admin\Auth\Logout` | Admin logout action |
   186→| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
   187→| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
   188→| Password reset | Standard Laravel flow with `Password::broker('users')` |
   189→
   190→**Customer auth requirements:**
   191→
   192→| Component | Description |
   193→|-----------|-------------|
   194→| Custom guard | `customer` guard with `customers` provider |
   195→| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
   196→| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
   197→| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
   198→| Uniqueness | Customer email unique per store, not globally |
   199→| Rate limiting | 5 attempts per minute per IP |
   200→
   201→**Rate limiter registration:**
   202→- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)
   203→
   204→#### Step 1.8: Authorization
   205→
   206→**Files to create:**
   207→
   208→| Type | Name | Purpose |
   209→|------|------|---------|
   210→| Policy | `ProductPolicy` | Product authorization based on store role |
   211→| Policy | `OrderPolicy` | Order authorization based on store role |
   212→| Policy | `CollectionPolicy` | Collection authorization based on store role |
   213→| Policy | `DiscountPolicy` | Discount authorization based on store role |
   214→| Policy | `CustomerPolicy` | Customer authorization based on store role |
   215→| Policy | `StorePolicy` | Store authorization based on store role |
   216→| Policy | `PagePolicy` | Page authorization based on store role |
   217→| Policy | `ThemePolicy` | Theme authorization based on store role |
   218→| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
   219→| Policy | `RefundPolicy` | Refund authorization based on store role |
   220→
   221→Each policy checks user role via `store_users` pivot for the current store. Helper on User model: `roleForStore(Store $store): ?StoreUserRole`. Register policies via auto-discovery or explicit binding in `AppServiceProvider`.
   222→
   223→**Permission Matrix:**
   224→
   225→| Permission | Owner | Admin | Staff | Support |
   226→|------------|:-----:|:-----:|:-----:|:-------:|
   227→| Manage store settings | Y | Y | N | N |
   228→| Delete store | Y | N | N | N |
   229→| Manage staff | Y | Y | N | N |
   230→| Manage products (CRUD) | Y | Y | Y | N |
   231→| Delete/archive products | Y | Y | N | N |
   232→| Manage orders | Y | Y | Y | Read-only |
   233→| Process refunds | Y | Y | N | N |
   234→| Create fulfillments | Y | Y | Y | N |
   235→| Manage discounts | Y | Y | Y | N |
   236→| Manage themes | Y | Y | N | N |
   237→| Manage apps | Y | Y | N | N |
   238→| View analytics | Y | Y | Y | N |
   239→| Manage shipping/tax | Y | Y | N | N |
   240→| View customers | Y | Y | Y | Y |
   241→| Update customers | Y | Y | Y | N |
   242→
   243→---
   244→
   245→### Phase 2: Catalog (Products, Variants, Inventory, Collections, Media)
   246→
   247→**Priority:** HIGH - storefront and orders depend on this.
   248→
   249→#### Step 2.1: Migrations (Batch 3-5)
   250→
   251→Create in dependency order:
   252→
   253→1. `create_products_table` - FK: `store_id`
   254→2. `create_product_options_table` - FK: `product_id`
   255→3. `create_product_option_values_table` - FK: `product_option_id`
   256→4. `create_product_variants_table` - FK: `product_id`
   257→5. `create_variant_option_values_table` - composite PK: `variant_id` + `product_option_value_id`
   258→6. `create_inventory_items_table` - FK: `store_id`, `variant_id`
   259→7. `create_collections_table` - FK: `store_id`
   260→8. `create_collection_products_table` - composite PK: `collection_id` + `product_id`
   261→9. `create_product_media_table` - FK: `product_id`
   262→
   263→#### Step 2.2: Models with Relationships
   264→
   265→**Files to create:**
   266→
   267→| Type | Name | Purpose |
   268→|------|------|---------|
   269→| Model (with migration, factory, seeder) | `Product` | Product entity |
   270→| Model (with migration, factory, seeder) | `ProductOption` | Product option (e.g., Size, Color) |
   271→| Model (with migration, factory, seeder) | `ProductOptionValue` | Option values (e.g., S, M, L) |
   272→| Model (with migration, factory, seeder) | `ProductVariant` | Product variant (SKU-level) |
   273→| Model (with migration, factory, seeder) | `InventoryItem` | Inventory tracking per variant |
   274→| Model (with migration, factory, seeder) | `Collection` | Product collection/category |
   275→| Model (with migration, factory, seeder) | `ProductMedia` | Product images and videos |
   276→
   277→**Relationship definitions:**
   278→
   279→| Model | Key Relationships |
   280→|-------|-------------------|
   281→| `Product` | `hasMany(ProductVariant)`, `hasMany(ProductOption)`, `hasMany(ProductMedia)`, `belongsToMany(Collection)` via `collection_products` |
   282→| `ProductOption` | `belongsTo(Product)`, `hasMany(ProductOptionValue)` |
   283→| `ProductOptionValue` | `belongsTo(ProductOption)` |
   284→| `ProductVariant` | `belongsTo(Product)`, `hasOne(InventoryItem)`, `belongsToMany(ProductOptionValue)` via `variant_option_values` |
   285→| `InventoryItem` | `belongsTo(ProductVariant)` |
   286→| `Collection` | `belongsToMany(Product)` via `collection_products` |
   287→| `ProductMedia` | `belongsTo(Product)` |
   288→
   289→**Enums to create:**
   290→
   291→| Enum | Values |
   292→|------|--------|
   293→| `ProductStatus` | `Draft`, `Active`, `Archived` |
   294→| `VariantStatus` | `Active`, `Archived` |
   295→| `CollectionStatus` | `Draft`, `Active`, `Archived` |
   296→| `MediaType` | `Image`, `Video` |
   297→| `MediaStatus` | `Processing`, `Ready`, `Failed` |
   298→| `InventoryPolicy` | `Deny`, `Continue` |
   299→
   300→#### Step 2.3: Product Service
   301→
   302→**File to create:** `App\Services\ProductService`
   303→
   304→| Method | Description |
   305→|--------|-------------|
   306→| `create(Store $store, array $data): Product` | Creates product with nested variants/options |
   307→| `update(Product $product, array $data): Product` | Updates product |
   308→| `transitionStatus(Product $product, ProductStatus $newStatus): void` | Validates transitions per state machine |
   309→| `delete(Product $product): void` | Hard delete only if draft with no order references; otherwise throws |
   310→
   311→**File to create:** `App\Services\VariantMatrixService`
   312→
   313→| Method | Description |
   314→|--------|-------------|
   315→| `rebuildMatrix(Product $product): void` | Computes cartesian product, creates missing variants, archives orphaned ones |
   316→
   317→**File to create:** `App\Support\HandleGenerator`
   318→
   319→| Method | Description |
   320→|--------|-------------|
   321→| `generate(string $title, string $table, int $storeId, ?int $excludeId = null): string` | Generates unique slugs |
   322→
   323→#### Step 2.4: Inventory Service
   324→
   325→**File to create:** `App\Services\InventoryService`
   326→
   327→| Method | Description |
   328→|--------|-------------|
   329→| `checkAvailability(InventoryItem $item, int $quantity): bool` | Checks if sufficient available inventory exists |
   330→| `reserve(InventoryItem $item, int $quantity): void` | Increments `quantity_reserved` |
   331→| `release(InventoryItem $item, int $quantity): void` | Decrements `quantity_reserved` |
   332→| `commit(InventoryItem $item, int $quantity): void` | Decrements both `on_hand` and `reserved` |
   333→| `restock(InventoryItem $item, int $quantity): void` | Increments `quantity_on_hand` |
   334→
   335→All operations wrapped in database transactions. Throws `InsufficientInventoryException` when policy is `deny` and available < quantity.
   336→
   337→#### Step 2.5: Media Upload
   338→
   339→**File to create:** `App\Jobs\ProcessMediaUpload`
   340→
   341→Requirements:
   342→- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
   343→- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)
   344→
   345→Livewire file upload via `WithFileUploads` trait. Stored on local `public` disk.
   346→
   347→---
   348→
   349→### Phase 3: Themes, Pages, Navigation, Storefront Layout
   350→
   351→**Priority:** HIGH - storefront rendering depends on this.
   352→
   353→#### Step 3.1: Migrations (Batch 3)
   354→
   355→1. `create_themes_table` - FK: `store_id`
   356→2. `create_theme_files_table` - FK: `theme_id`
   357→3. `create_theme_settings_table` - PK: `theme_id`
   358→4. `create_pages_table` - FK: `store_id`
   359→5. `create_navigation_menus_table` - FK: `store_id`
   360→6. `create_navigation_items_table` - FK: `menu_id`
   361→
   362→#### Step 3.2: Models
   363→
   364→**Files to create:**
   365→
   366→| Type | Name | Purpose |
   367→|------|------|---------|
   368→| Model (with migration, factory, seeder) | `Theme` | Store theme entity |
   369→| Model (with migration, factory, seeder) | `ThemeFile` | Individual theme file |
   370→| Model (with migration, factory, seeder) | `ThemeSettings` | Theme configuration |
   371→| Model (with migration, factory, seeder) | `Page` | CMS page |
   372→| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
   373→| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |
   374→
   375→**Relationship definitions:**
   376→
   377→| Model | Key Relationships |
   378→|-------|-------------------|
   379→| `Theme` | `belongsTo(Store)`, `hasMany(ThemeFile)`, `hasOne(ThemeSettings)` |
   380→| `ThemeFile` | `belongsTo(Theme)` |
   381→| `ThemeSettings` | `belongsTo(Theme)` |
   382→| `Page` | `belongsTo(Store)` |
   383→| `NavigationMenu` | `belongsTo(Store)`, `hasMany(NavigationItem)` |
   384→| `NavigationItem` | `belongsTo(NavigationMenu)` |
   385→
   386→**Enums to create:**
   387→
   388→| Enum | Values |
   389→|------|--------|
   390→| `ThemeStatus` | `Draft`, `Published` |
   391→| `PageStatus` | `Draft`, `Published`, `Archived` |
   392→| `NavigationItemType` | `Link`, `Page`, `Collection`, `Product` |
   393→
   394→#### Step 3.3: Storefront Blade Layout
   395→
   396→**Directory structure for `resources/views/storefront/`:**
   397→
   398→| Directory | Files |
   399→|-----------|-------|
   400→| `layouts/` | `app.blade.php` |
   401→| `pages/` | `home.blade.php`, `collection.blade.php`, `product.blade.php`, `cart.blade.php`, `search.blade.php`, `page.blade.php` |
   402→| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
   403→| `account/` | `login.blade.php`, `register.blade.php`, `dashboard.blade.php` |
   404→| `account/orders/` | `index.blade.php`, `show.blade.php` |
   405→| `account/addresses/` | `index.blade.php` |
   406→| `errors/` | `404.blade.php`, `503.blade.php` |
   407→| `components/` | `product-card.blade.php`, `price.blade.php`, `badge.blade.php`, `quantity-selector.blade.php`, `address-form.blade.php`, `order-summary.blade.php`, `breadcrumbs.blade.php`, `pagination.blade.php` |
   408→
   409→Base layout (`app.blade.php`): header with nav, announcement bar, main content, footer, cart drawer. All with dark mode support via `dark:` prefix.
   410→
   411→**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.
   412→
   413→#### Step 3.4: Storefront Livewire Components
   414→
   415→**Files to create:**
   416→
   417→| Component | Purpose |
   418→|-----------|---------|
   419→| `Storefront\Home` | Dynamic sections based on theme settings |
   420→| `Storefront\Collections\Index` | All collections grid |
   421→| `Storefront\Collections\Show` | Product grid with filters/sort/pagination |
   422→| `Storefront\Products\Show` | Variant selection, image gallery, add-to-cart |
   423→| `Storefront\Cart\Show` | Full cart page with line items and totals |
   424→| `Storefront\CartDrawer` | Slide-out cart drawer (always in layout) |
   425→| `Storefront\Search\Index` | Search results with filters |
   426→| `Storefront\Search\Modal` | Search modal with autocomplete |
   427→| `Storefront\Pages\Show` | Static CMS page rendering |
   428→
   429→#### Step 3.5: Navigation Service
   430→
   431→**File to create:** `App\Services\NavigationService`
   432→
   433→| Method | Description |
   434→|--------|-------------|
   435→| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
   436→| `resolveUrl(NavigationItem $item): string` | Resolves URLs for page, collection, product type items |
   437→
   438→Caches per store with 5-minute TTL.
   439→
   440→---
   441→
   442→### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes
   443→
   444→**Priority:** HIGH - core shopping flow.
   445→
   446→#### Step 4.1: Migrations (Batch 4-5)
   447→
   448→1. `create_carts_table` - FK: `store_id`, `customer_id`
   449→2. `create_cart_lines_table` - FK: `cart_id`, `variant_id`
   450→3. `create_checkouts_table` - FK: `store_id`, `cart_id`, `customer_id`
   451→4. `create_shipping_zones_table` - FK: `store_id`
   452→5. `create_shipping_rates_table` - FK: `zone_id`
   453→6. `create_tax_settings_table` - PK: `store_id`
   454→7. `create_discounts_table` - FK: `store_id`
   455→
   456→#### Step 4.2: Models
   457→
   458→**Files to create:**
   459→
   460→| Type | Name | Purpose |
   461→|------|------|---------|
   462→| Model (with migration, factory, seeder) | `Cart` | Shopping cart entity |
   463→| Model (with migration, factory, seeder) | `CartLine` | Individual cart line item |
   464→| Model (with migration, factory, seeder) | `Checkout` | Checkout session entity |
   465→| Model (with migration, factory, seeder) | `ShippingZone` | Shipping zone configuration |
   466→| Model (with migration, factory, seeder) | `ShippingRate` | Shipping rate within a zone |
   467→| Model (with migration, factory, seeder) | `TaxSettings` | Per-store tax configuration |
   468→| Model (with migration, factory, seeder) | `Discount` | Discount code or automatic discount |
   469→
   470→**Relationship definitions:**
   471→
   472→| Model | Key Relationships |
   473→|-------|-------------------|
   474→| `Cart` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(CartLine)`, `hasMany(Checkout)` |
   475→| `CartLine` | `belongsTo(Cart)`, `belongsTo(ProductVariant)` |
   476→| `Checkout` | `belongsTo(Store)`, `belongsTo(Cart)`, `belongsTo(Customer)` |
   477→| `ShippingZone` | `belongsTo(Store)`, `hasMany(ShippingRate)` |
   478→| `ShippingRate` | `belongsTo(ShippingZone)` |
   479→| `TaxSettings` | `belongsTo(Store)` |
   480→| `Discount` | `belongsTo(Store)` |
   481→
   482→**Enums to create:**
   483→
   484→| Enum | Values |
   485→|------|--------|
   486→| `CartStatus` | `Active`, `Converted`, `Abandoned` |
   487→| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
   488→| `DiscountType` | `Code`, `Automatic` |
   489→| `DiscountValueType` | `Percent` (`percent`), `Fixed` (`fixed`), `FreeShipping` (`free_shipping`) -- PascalCase cases backed by lowercase string values stored in DB |
   490→| `DiscountStatus` | `Draft`, `Active`, `Expired`, `Disabled` |
   491→| `ShippingRateType` | `Flat`, `Weight`, `Price`, `Carrier` |
   492→| `TaxMode` | `Manual`, `Provider` |
   493→
   494→#### Step 4.3: Cart Service
   495→
   496→**File to create:** `App\Services\CartService`
   497→
   498→| Method | Description |
   499→|--------|-------------|
   500→| `create(Store, ?Customer): Cart` | Creates cart with store currency, version 1 |

# 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)

Create in this dependency order:

1. `create_organizations_table` - no dependencies
2. `create_stores_table` - FK: `organization_id`
3. `create_store_domains_table` - FK: `store_id`
4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
6. `create_store_settings_table` - PK: `store_id`

All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.

#### Step 1.3: Core Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Organization` | `hasMany(Store::class)` |
| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
| `StoreDomain` | `belongsTo(Store::class)` |
| `User` | `belongsToMany(Store::class)` through `store_users` |
| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
| `StoreSettings` | `belongsTo(Store::class)` |

Each model must have:
- Explicit return type declarations on all methods
- Proper `$fillable` or `$guarded` arrays
- `casts()` method for JSON/enum columns
- Factory with meaningful default values
- Seeder that creates sample data

#### Step 1.4: Enums

**Files to create in `app/Enums/`:**

| Enum | Values |
|------|--------|
| `StoreStatus` | `Active`, `Suspended` |
| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |
| `StoreDomainType` | `Storefront`, `Admin`, `Api` |

#### Step 1.5: Tenant Resolution Middleware

**File to create:** `App\Http\Middleware\ResolveStore`

Requirements:
1. Storefront routes: extract hostname from the request, look up `store_domains`, cache hostname-to-store_id for 5 minutes, bind `Store` as singleton via container instance `current_store`
2. Admin routes: read `current_store_id` from session, verify user has `store_users` record
3. Return 404 if hostname not found
4. Return 503 for suspended stores on storefront routes

**Registration in `bootstrap/app.php`:**
- Register middleware group `storefront` containing `ResolveStore`
- Register middleware group `admin` containing `ResolveStore`

#### Step 1.6: BelongsToStore Trait and Global Scope

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Trait | `App\Models\Concerns\BelongsToStore` | Applies StoreScope and auto-sets store_id on creating event |
| Scope | `App\Models\Scopes\StoreScope` | Applies `where('store_id', app('current_store')->id)` to all queries |

Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.

#### Step 1.7: Authentication

**Admin auth requirements:**

| Component | Description |
|-----------|-------------|
| Livewire component `Admin\Auth\Login` | Admin login form |
| Livewire component `Admin\Auth\Logout` | Admin logout action |
| Auth mechanism | Standard Laravel session auth via `Auth::guard('web')->attempt()` |
| Rate limiting | 5 attempts per minute per IP via `RateLimiter::for('login')` |
| Password reset | Standard Laravel flow with `Password::broker('users')` |

**Customer auth requirements:**

| Component | Description |
|-----------|-------------|
| Custom guard | `customer` guard with `customers` provider |
| Custom user provider | `CustomerUserProvider` that injects `store_id` into queries |
| Livewire component `Storefront\Account\Auth\Login` | Customer login form |
| Livewire component `Storefront\Account\Auth\Register` | Customer registration form |
| Uniqueness | Customer email unique per store, not globally |
| Rate limiting | 5 attempts per minute per IP |

**Rate limiter registration:**
- Register rate limiters in `AppServiceProvider::boot()` for login (5 per minute per IP)

#### Step 1.8: Authorization

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Policy | `ProductPolicy` | Product authorization based on store role |
| Policy | `OrderPolicy` | Order authorization based on store role |
| Policy | `CollectionPolicy` | Collection authorization based on store role |
| Policy | `DiscountPolicy` | Discount authorization based on store role |
| Policy | `CustomerPolicy` | Customer authorization based on store role |
| Policy | `StorePolicy` | Store authorization based on store role |
| Policy | `PagePolicy` | Page authorization based on store role |
| Policy | `ThemePolicy` | Theme authorization based on store role |
| Policy | `FulfillmentPolicy` | Fulfillment authorization based on store role |
| Policy | `RefundPolicy` | Refund authorization based on store role |

Each policy checks user role via `store_users` pivot for the current store. Helper on User model: `roleForStore(Store $store): ?StoreUserRole`. Register policies via auto-discovery or explicit binding in `AppServiceProvider`.

**Permission Matrix:**

| Permission | Owner | Admin | Staff | Support |
|------------|:-----:|:-----:|:-----:|:-------:|
| Manage store settings | Y | Y | N | N |
| Delete store | Y | N | N | N |
| Manage staff | Y | Y | N | N |
| Manage products (CRUD) | Y | Y | Y | N |
| Delete/archive products | Y | Y | N | N |
| Manage orders | Y | Y | Y | Read-only |
| Process refunds | Y | Y | N | N |
| Create fulfillments | Y | Y | Y | N |
| Manage discounts | Y | Y | Y | N |
| Manage themes | Y | Y | N | N |
| Manage apps | Y | Y | N | N |
| View analytics | Y | Y | Y | N |
| Manage shipping/tax | Y | Y | N | N |
| View customers | Y | Y | Y | Y |
| Update customers | Y | Y | Y | N |

---

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

**Priority:** HIGH - storefront and orders depend on this.

#### Step 2.1: Migrations (Batch 3-5)

Create in dependency order:

1. `create_products_table` - FK: `store_id`
2. `create_product_options_table` - FK: `product_id`
3. `create_product_option_values_table` - FK: `product_option_id`
4. `create_product_variants_table` - FK: `product_id`
5. `create_variant_option_values_table` - composite PK: `variant_id` + `product_option_value_id`
6. `create_inventory_items_table` - FK: `store_id`, `variant_id`
7. `create_collections_table` - FK: `store_id`
8. `create_collection_products_table` - composite PK: `collection_id` + `product_id`
9. `create_product_media_table` - FK: `product_id`

#### Step 2.2: Models with Relationships

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Product` | Product entity |
| Model (with migration, factory, seeder) | `ProductOption` | Product option (e.g., Size, Color) |
| Model (with migration, factory, seeder) | `ProductOptionValue` | Option values (e.g., S, M, L) |
| Model (with migration, factory, seeder) | `ProductVariant` | Product variant (SKU-level) |
| Model (with migration, factory, seeder) | `InventoryItem` | Inventory tracking per variant |
| Model (with migration, factory, seeder) | `Collection` | Product collection/category |
| Model (with migration, factory, seeder) | `ProductMedia` | Product images and videos |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Product` | `hasMany(ProductVariant)`, `hasMany(ProductOption)`, `hasMany(ProductMedia)`, `belongsToMany(Collection)` via `collection_products` |
| `ProductOption` | `belongsTo(Product)`, `hasMany(ProductOptionValue)` |
| `ProductOptionValue` | `belongsTo(ProductOption)` |
| `ProductVariant` | `belongsTo(Product)`, `hasOne(InventoryItem)`, `belongsToMany(ProductOptionValue)` via `variant_option_values` |
| `InventoryItem` | `belongsTo(ProductVariant)` |
| `Collection` | `belongsToMany(Product)` via `collection_products` |
| `ProductMedia` | `belongsTo(Product)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `ProductStatus` | `Draft`, `Active`, `Archived` |
| `VariantStatus` | `Active`, `Archived` |
| `CollectionStatus` | `Draft`, `Active`, `Archived` |
| `MediaType` | `Image`, `Video` |
| `MediaStatus` | `Processing`, `Ready`, `Failed` |
| `InventoryPolicy` | `Deny`, `Continue` |

#### Step 2.3: Product Service

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

| Method | Description |
|--------|-------------|
| `create(Store $store, array $data): Product` | Creates product with nested variants/options |
| `update(Product $product, array $data): Product` | Updates product |
| `transitionStatus(Product $product, ProductStatus $newStatus): void` | Validates transitions per state machine |
| `delete(Product $product): void` | Hard delete only if draft with no order references; otherwise throws |

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

| Method | Description |
|--------|-------------|
| `rebuildMatrix(Product $product): void` | Computes cartesian product, creates missing variants, archives orphaned ones |

**File to create:** `App\Support\HandleGenerator`

| Method | Description |
|--------|-------------|
| `generate(string $title, string $table, int $storeId, ?int $excludeId = null): string` | Generates unique slugs |

#### Step 2.4: Inventory Service

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

| Method | Description |
|--------|-------------|
| `checkAvailability(InventoryItem $item, int $quantity): bool` | Checks if sufficient available inventory exists |
| `reserve(InventoryItem $item, int $quantity): void` | Increments `quantity_reserved` |
| `release(InventoryItem $item, int $quantity): void` | Decrements `quantity_reserved` |
| `commit(InventoryItem $item, int $quantity): void` | Decrements both `on_hand` and `reserved` |
| `restock(InventoryItem $item, int $quantity): void` | Increments `quantity_on_hand` |

All operations wrapped in database transactions. Throws `InsufficientInventoryException` when policy is `deny` and available < quantity.

#### Step 2.5: Media Upload

**File to create:** `App\Jobs\ProcessMediaUpload`

Requirements:
- Resizes images to standard sizes (thumbnail 150x150, medium 600x600, large 1200x1200)
- Updates `ProductMedia` status from `processing` to `ready` (or `failed`)

Livewire file upload via `WithFileUploads` trait. Stored on local `public` disk.

---

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

**Priority:** HIGH - storefront rendering depends on this.

#### Step 3.1: Migrations (Batch 3)

1. `create_themes_table` - FK: `store_id`
2. `create_theme_files_table` - FK: `theme_id`
3. `create_theme_settings_table` - PK: `theme_id`
4. `create_pages_table` - FK: `store_id`
5. `create_navigation_menus_table` - FK: `store_id`
6. `create_navigation_items_table` - FK: `menu_id`

#### Step 3.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Theme` | Store theme entity |
| Model (with migration, factory, seeder) | `ThemeFile` | Individual theme file |
| Model (with migration, factory, seeder) | `ThemeSettings` | Theme configuration |
| Model (with migration, factory, seeder) | `Page` | CMS page |
| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Theme` | `belongsTo(Store)`, `hasMany(ThemeFile)`, `hasOne(ThemeSettings)` |
| `ThemeFile` | `belongsTo(Theme)` |
| `ThemeSettings` | `belongsTo(Theme)` |
| `Page` | `belongsTo(Store)` |
| `NavigationMenu` | `belongsTo(Store)`, `hasMany(NavigationItem)` |
| `NavigationItem` | `belongsTo(NavigationMenu)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `ThemeStatus` | `Draft`, `Published` |
| `PageStatus` | `Draft`, `Published`, `Archived` |
| `NavigationItemType` | `Link`, `Page`, `Collection`, `Product` |

#### Step 3.3: Storefront Blade Layout

**Directory structure for `resources/views/storefront/`:**

| Directory | Files |
|-----------|-------|
| `layouts/` | `app.blade.php` |
| `pages/` | `home.blade.php`, `collection.blade.php`, `product.blade.php`, `cart.blade.php`, `search.blade.php`, `page.blade.php` |
| `checkout/` | `index.blade.php`, `confirmation.blade.php` |
| `account/` | `login.blade.php`, `register.blade.php`, `dashboard.blade.php` |
| `account/orders/` | `index.blade.php`, `show.blade.php` |
| `account/addresses/` | `index.blade.php` |
| `errors/` | `404.blade.php`, `503.blade.php` |
| `components/` | `product-card.blade.php`, `price.blade.php`, `badge.blade.php`, `quantity-selector.blade.php`, `address-form.blade.php`, `order-summary.blade.php`, `breadcrumbs.blade.php`, `pagination.blade.php` |

Base layout (`app.blade.php`): header with nav, announcement bar, main content, footer, cart drawer. All with dark mode support via `dark:` prefix.

**ThemeSettings Service:** Singleton registered in `AppServiceProvider`, loads and caches active theme settings for the current store.

#### Step 3.4: Storefront Livewire Components

**Files to create:**

| Component | Purpose |
|-----------|---------|
| `Storefront\Home` | Dynamic sections based on theme settings |
| `Storefront\Collections\Index` | All collections grid |
| `Storefront\Collections\Show` | Product grid with filters/sort/pagination |
| `Storefront\Products\Show` | Variant selection, image gallery, add-to-cart |
| `Storefront\Cart\Show` | Full cart page with line items and totals |
| `Storefront\CartDrawer` | Slide-out cart drawer (always in layout) |
| `Storefront\Search\Index` | Search results with filters |
| `Storefront\Search\Modal` | Search modal with autocomplete |
| `Storefront\Pages\Show` | Static CMS page rendering |

#### Step 3.5: Navigation Service

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

| Method | Description |
|--------|-------------|
| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
| `resolveUrl(NavigationItem $item): string` | Resolves URLs for page, collection, product type items |

Caches per store with 5-minute TTL.

---

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

**Priority:** HIGH - core shopping flow.

#### Step 4.1: Migrations (Batch 4-5)

1. `create_carts_table` - FK: `store_id`, `customer_id`
2. `create_cart_lines_table` - FK: `cart_id`, `variant_id`
3. `create_checkouts_table` - FK: `store_id`, `cart_id`, `customer_id`
4. `create_shipping_zones_table` - FK: `store_id`
5. `create_shipping_rates_table` - FK: `zone_id`
6. `create_tax_settings_table` - PK: `store_id`
7. `create_discounts_table` - FK: `store_id`

#### Step 4.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Cart` | Shopping cart entity |
| Model (with migration, factory, seeder) | `CartLine` | Individual cart line item |
| Model (with migration, factory, seeder) | `Checkout` | Checkout session entity |
| Model (with migration, factory, seeder) | `ShippingZone` | Shipping zone configuration |
| Model (with migration, factory, seeder) | `ShippingRate` | Shipping rate within a zone |
| Model (with migration, factory, seeder) | `TaxSettings` | Per-store tax configuration |
| Model (with migration, factory, seeder) | `Discount` | Discount code or automatic discount |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Cart` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(CartLine)`, `hasMany(Checkout)` |
| `CartLine` | `belongsTo(Cart)`, `belongsTo(ProductVariant)` |
| `Checkout` | `belongsTo(Store)`, `belongsTo(Cart)`, `belongsTo(Customer)` |
| `ShippingZone` | `belongsTo(Store)`, `hasMany(ShippingRate)` |
| `ShippingRate` | `belongsTo(ShippingZone)` |
| `TaxSettings` | `belongsTo(Store)` |
| `Discount` | `belongsTo(Store)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `CartStatus` | `Active`, `Converted`, `Abandoned` |
| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
| `DiscountType` | `Code`, `Automatic` |
| `DiscountValueType` | `Percent` (`percent`), `Fixed` (`fixed`), `FreeShipping` (`free_shipping`) -- PascalCase cases backed by lowercase string values stored in DB |
| `DiscountStatus` | `Draft`, `Active`, `Expired`, `Disabled` |
| `ShippingRateType` | `Flat`, `Weight`, `Price`, `Carrier` |
| `TaxMode` | `Manual`, `Provider` |

#### Step 4.3: Cart Service

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

| Method | Description |
|--------|-------------|
| `create(Store, ?Customer): Cart` | Creates cart with store currency, version 1 |


Let me continue reading the rest of the roadmap and other spec files.