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


<local-command-stdout>MCP dialog dismissed</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 sub-agents! 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.


The user wants me to implement an entire shop system based on specifications in `specs/`. This is a massive undertaking that requires:

1. Reading all specs
2. Following the implementation roadmap
3. Building everything with sub-agents
4. Testing with Pest
5. Testing with Playwright
6. Tracking progress
7. Committing regularly
8. Final review/showcase

Let me start by reading the implementation roadmap and all specs to understand the full scope, then plan and execute with sub-agents.

Let me begin by reading the specs files to understand what needs to be built.


I'll start by reading all the specs to understand the full scope, then execute the implementation using sub-agents. Let me read everything first.


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>


The roadmap file is too large. Let me read it in parts.


Let me read the specs in parts.


     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

# 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


     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 |
    51→| name | TEXT | No | - | - | Store display name |
    52→| handle | TEXT | No | - | UNIQUE per table | URL-friendly slug |
    53→| status | TEXT | No | 'active' | CHECK IN ('active', 'suspended') | |
    54→| default_currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
    55→| default_locale | TEXT | No | 'en' | - | IETF locale tag |
    56→| timezone | TEXT | No | 'UTC' | - | IANA timezone |
    57→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    58→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    59→
    60→**Indexes:**
    61→
    62→- `idx_stores_handle` UNIQUE on (`handle`) - Ensures globally unique store handles for URL routing
    63→- `idx_stores_organization_id` on (`organization_id`) - FK lookup and listing stores by org
    64→- `idx_stores_status` on (`status`) - Filtering stores by active/suspended status
    65→
    66→---
    67→
    68→### store_domains
    69→
    70→Custom domains attached to a store.
    71→
    72→| Column | Type | Nullable | Default | Constraints | Notes |
    73→|---|---|---|---|---|---|
    74→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    75→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
    76→| hostname | TEXT | No | - | UNIQUE per table | e.g. "shop.example.com" |
    77→| type | TEXT | No | 'storefront' | CHECK IN ('storefront', 'admin', 'api') | Domain purpose |
    78→| is_primary | INTEGER | No | 0 | - | Boolean: primary domain flag |
    79→| tls_mode | TEXT | No | 'managed' | CHECK IN ('managed', 'bring_your_own') | TLS certificate mode |
    80→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    81→
    82→**Indexes:**
    83→
    84→- `idx_store_domains_hostname` UNIQUE on (`hostname`) - Globally unique hostname for routing
    85→- `idx_store_domains_store_id` on (`store_id`) - FK lookup, listing domains per store
    86→- `idx_store_domains_store_primary` on (`store_id`, `is_primary`) - Quick lookup of a store's primary domain
    87→
    88→---
    89→
    90→### users
    91→
    92→Admin/staff users who manage stores (not storefront customers).
    93→
    94→| Column | Type | Nullable | Default | Constraints | Notes |
    95→|---|---|---|---|---|---|
    96→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    97→| email | TEXT | No | - | UNIQUE per table | Login email |
    98→| password_hash | TEXT | No | - | - | bcrypt/argon2 hash; maps to Laravel's `password` field |
    99→| name | TEXT | No | - | - | Display name |
   100→| status | TEXT | No | 'active' | CHECK IN ('active', 'disabled') | |
   101→| email_verified_at | TEXT | Yes | NULL | - | ISO-8601 timestamp; required for `verified` middleware |
   102→| last_login_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   103→| two_factor_secret | TEXT | Yes | NULL | - | TOTP secret |
   104→| two_factor_recovery_codes | TEXT | Yes | NULL | - | JSON array of recovery codes |
   105→| two_factor_confirmed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   106→| remember_token | TEXT | Yes | NULL | - | Laravel remember-me token |
   107→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   108→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   109→
   110→**Indexes:**
   111→
   112→- `idx_users_email` UNIQUE on (`email`) - Enforces unique email, supports login lookup
   113→- `idx_users_status` on (`status`) - Filtering users by active/disabled
   114→
   115→**Notes:** The `two_factor_*` columns align with the existing migration `2025_08_14_170933_add_two_factor_columns_to_users_table.php`. The `password_hash` column maps to Laravel's default `password` field internally but stores a bcrypt/argon2 hash.
   116→
   117→---
   118→
   119→### store_users
   120→
   121→Pivot table linking users to stores with a role. Uses composite primary key.
   122→
   123→| Column | Type | Nullable | Default | Constraints | Notes |
   124→|---|---|---|---|---|---|
   125→| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
   126→| user_id | INTEGER | No | - | PK (composite), FK -> users(id) ON DELETE CASCADE | |
   127→| role | TEXT | No | 'staff' | CHECK IN ('owner', 'admin', 'staff', 'support') | |
   128→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   129→
   130→**Primary Key:** (`store_id`, `user_id`)
   131→
   132→**Indexes:**
   133→
   134→- `idx_store_users_user_id` on (`user_id`) - Reverse lookup: find all stores for a user
   135→- `idx_store_users_role` on (`store_id`, `role`) - Listing users by role within a store
   136→
   137→---
   138→
   139→### store_settings
   140→
   141→Single-row-per-store JSON settings bag.
   142→
   143→| Column | Type | Nullable | Default | Constraints | Notes |
   144→|---|---|---|---|---|---|
   145→| store_id | INTEGER | No | - | PRIMARY KEY, FK -> stores(id) ON DELETE CASCADE | One-to-one with stores |
   146→| settings_json | TEXT | No | '{}' | - | JSON settings object |
   147→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   148→
   149→**Notes:** No additional indexes needed - primary key on `store_id` covers all queries.
   150→
   151→---
   152→
   153→## Epic 2 - Catalog, Collections, Inventory, and Media
   154→
   155→### products
   156→
   157→Core product record, scoped to a store.
   158→
   159→| Column | Type | Nullable | Default | Constraints | Notes |
   160→|---|---|---|---|---|---|
   161→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   162→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
   163→| title | TEXT | No | - | - | Product display title |
   164→| handle | TEXT | No | - | UNIQUE per (store_id, handle) | URL-friendly slug |
   165→| status | TEXT | No | 'draft' | CHECK IN ('draft', 'active', 'archived') | |
   166→| description_html | TEXT | Yes | NULL | - | Rich-text description |
   167→| vendor | TEXT | Yes | NULL | - | Manufacturer/brand |
   168→| product_type | TEXT | Yes | NULL | - | Category classification |
   169→| tags | TEXT | No | '[]' | - | JSON array, e.g. ["summer","sale"] |
   170→| published_at | TEXT | Yes | NULL | - | ISO-8601 timestamp; null = unpublished |
   171→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   172→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   173→
   174→**Indexes:**
   175→
   176→- `idx_products_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
   177→- `idx_products_store_id` on (`store_id`) - FK lookup, listing all products in a store
   178→- `idx_products_store_status` on (`store_id`, `status`) - Filtering products by status within a store
   179→- `idx_products_published_at` on (`store_id`, `published_at`) - Sorting/filtering by publication date
   180→- `idx_products_vendor` on (`store_id`, `vendor`) - Filtering products by vendor
   181→- `idx_products_product_type` on (`store_id`, `product_type`) - Filtering products by type
   182→
   183→**Notes:** `tags` is a JSON-encoded array (e.g. `["summer","sale"]`). The `handle` is unique per store, not globally.
   184→
   185→---
   186→
   187→### product_options
   188→
   189→Named option dimensions for a product (e.g. Size, Color).
   190→
   191→| Column | Type | Nullable | Default | Constraints | Notes |
   192→|---|---|---|---|---|---|
   193→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   194→| product_id | INTEGER | No | - | FK -> products(id) ON DELETE CASCADE | Parent product |
   195→| name | TEXT | No | - | - | Option name, e.g. "Size", "Color" |
   196→| position | INTEGER | No | 0 | UNIQUE per (product_id, position) | Display ordering |
   197→
   198→**Indexes:**
   199→
   200→- `idx_product_options_product_id` on (`product_id`) - FK lookup, listing options per product
   201→- `idx_product_options_product_position` UNIQUE on (`product_id`, `position`) - Enforces unique ordering per product
   202→
   203→---
   204→
   205→### product_option_values
   206→
   207→Individual values within a product option (e.g. "Small", "Medium", "Large").
   208→
   209→| Column | Type | Nullable | Default | Constraints | Notes |
   210→|---|---|---|---|---|---|
   211→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   212→| product_option_id | INTEGER | No | - | FK -> product_options(id) ON DELETE CASCADE | Parent option |
   213→| value | TEXT | No | - | - | Display value, e.g. "Small" |
   214→| position | INTEGER | No | 0 | UNIQUE per (product_option_id, position) | Display ordering |
   215→
   216→**Indexes:**
   217→
   218→- `idx_product_option_values_option_id` on (`product_option_id`) - FK lookup, listing values per option
   219→- `idx_product_option_values_option_position` UNIQUE on (`product_option_id`, `position`) - Enforces unique ordering per option
   220→
   221→---
   222→
   223→### product_variants
   224→
   225→Purchasable SKU-level variant of a product.
   226→
   227→| Column | Type | Nullable | Default | Constraints | Notes |
   228→|---|---|---|---|---|---|
   229→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   230→| product_id | INTEGER | No | - | FK -> products(id) ON DELETE CASCADE | Parent product |
   231→| sku | TEXT | Yes | NULL | - | Stock keeping unit |
   232→| barcode | TEXT | Yes | NULL | - | UPC/EAN barcode |
   233→| price_amount | INTEGER | No | 0 | - | Price in minor units (cents) |
   234→| compare_at_amount | INTEGER | Yes | NULL | - | Original/compare price in minor units |
   235→| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
   236→| weight_g | INTEGER | Yes | NULL | - | Weight in grams |
   237→| requires_shipping | INTEGER | No | 1 | - | Boolean: physical product flag |
   238→| is_default | INTEGER | No | 0 | - | Boolean: default variant flag |
   239→| position | INTEGER | No | 0 | - | Display ordering |
   240→| status | TEXT | No | 'active' | CHECK IN ('active', 'archived') | |
   241→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   242→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
   243→
   244→**Indexes:**
   245→
   246→- `idx_product_variants_product_id` on (`product_id`) - FK lookup, listing variants per product
   247→- `idx_product_variants_sku` on (`sku`) - SKU lookup across all variants
   248→- `idx_product_variants_barcode` on (`barcode`) - Barcode scanning lookup
   249→- `idx_product_variants_product_position` on (`product_id`, `position`) - Ordered variant listing
   250→- `idx_product_variants_product_default` on (`product_id`, `is_default`) - Quick lookup of default variant
   251→
   252→**Notes:** `price_amount` and `compare_at_amount` are in minor currency units (cents). A product always has at least one variant (the default).
   253→
   254→---
   255→
   256→### variant_option_values
   257→
   258→Maps a variant to its chosen option values (e.g. variant #5 = Size:Large + Color:Red). Junction table with composite primary key.
   259→
   260→| Column | Type | Nullable | Default | Constraints | Notes |
   261→|---|---|---|---|---|---|
   262→| variant_id | INTEGER | No | - | PK (composite), FK -> product_variants(id) ON DELETE CASCADE | |
   263→| product_option_value_id | INTEGER | No | - | PK (composite), FK -> product_option_values(id) ON DELETE CASCADE | |
   264→
   265→**Primary Key:** (`variant_id`, `product_option_value_id`)
   266→
   267→**Indexes:**
   268→
   269→- `idx_variant_option_values_value_id` on (`product_option_value_id`) - Reverse lookup: find all variants with a given option value
   270→
   271→**Notes:** This is a junction table that links variants to the specific option values they represent. Required to resolve which combination of options each variant corresponds to.
   272→
   273→---
   274→
   275→### inventory_items
   276→
   277→Stock tracking per variant per store.
   278→
   279→| Column | Type | Nullable | Default | Constraints | Notes |
   280→|---|---|---|---|---|---|
   281→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
   282→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
   283→| variant_id | INTEGER | No | - | FK -> product_variants(id) ON DELETE CASCADE, UNIQUE | One inventory record per variant |
   284→| quantity_on_hand | INTEGER | No | 0 | - | Total physical stock |
   285→| quantity_reserved | INTEGER | No | 0 | - | Stock allocated to open orders |
   286→| policy | TEXT | No | 'deny' | CHECK IN ('deny', 'continue') | Oversell policy |
   287→
   288→**Indexes:**
   289→
   290→- `idx_inventory_items_variant_id` UNIQUE on (`variant_id`) - Enforces one inventory record per variant, fast variant lookup
   291→- `idx_inventory_items_store_id` on (`store_id`) - Listing all inventory for a store
   292→
   293→**Notes:** `quantity_on_hand` is the total physical stock. `quantity_reserved` is stock allocated to open orders not yet fulfilled. Available = on_hand - reserved. Policy `deny` prevents overselling; `continue` allows selling below zero.
   294→
   295→---
   296→
   297→### collections
   298→
   299→Curated or rule-based product groupings.
   300→

# 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 |
| name | TEXT | No | - | - | Store display name |
| handle | TEXT | No | - | UNIQUE per table | URL-friendly slug |
| status | TEXT | No | 'active' | CHECK IN ('active', 'suspended') | |
| default_currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| default_locale | TEXT | No | 'en' | - | IETF locale tag |
| timezone | TEXT | No | 'UTC' | - | IANA timezone |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_stores_handle` UNIQUE on (`handle`) - Ensures globally unique store handles for URL routing
- `idx_stores_organization_id` on (`organization_id`) - FK lookup and listing stores by org
- `idx_stores_status` on (`status`) - Filtering stores by active/suspended status

---

### store_domains

Custom domains attached to a store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| hostname | TEXT | No | - | UNIQUE per table | e.g. "shop.example.com" |
| type | TEXT | No | 'storefront' | CHECK IN ('storefront', 'admin', 'api') | Domain purpose |
| is_primary | INTEGER | No | 0 | - | Boolean: primary domain flag |
| tls_mode | TEXT | No | 'managed' | CHECK IN ('managed', 'bring_your_own') | TLS certificate mode |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_store_domains_hostname` UNIQUE on (`hostname`) - Globally unique hostname for routing
- `idx_store_domains_store_id` on (`store_id`) - FK lookup, listing domains per store
- `idx_store_domains_store_primary` on (`store_id`, `is_primary`) - Quick lookup of a store's primary domain

---

### users

Admin/staff users who manage stores (not storefront customers).

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| email | TEXT | No | - | UNIQUE per table | Login email |
| password_hash | TEXT | No | - | - | bcrypt/argon2 hash; maps to Laravel's `password` field |
| name | TEXT | No | - | - | Display name |
| status | TEXT | No | 'active' | CHECK IN ('active', 'disabled') | |
| email_verified_at | TEXT | Yes | NULL | - | ISO-8601 timestamp; required for `verified` middleware |
| last_login_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| two_factor_secret | TEXT | Yes | NULL | - | TOTP secret |
| two_factor_recovery_codes | TEXT | Yes | NULL | - | JSON array of recovery codes |
| two_factor_confirmed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| remember_token | TEXT | Yes | NULL | - | Laravel remember-me token |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_users_email` UNIQUE on (`email`) - Enforces unique email, supports login lookup
- `idx_users_status` on (`status`) - Filtering users by active/disabled

**Notes:** The `two_factor_*` columns align with the existing migration `2025_08_14_170933_add_two_factor_columns_to_users_table.php`. The `password_hash` column maps to Laravel's default `password` field internally but stores a bcrypt/argon2 hash.

---

### store_users

Pivot table linking users to stores with a role. Uses composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| user_id | INTEGER | No | - | PK (composite), FK -> users(id) ON DELETE CASCADE | |
| role | TEXT | No | 'staff' | CHECK IN ('owner', 'admin', 'staff', 'support') | |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Primary Key:** (`store_id`, `user_id`)

**Indexes:**

- `idx_store_users_user_id` on (`user_id`) - Reverse lookup: find all stores for a user
- `idx_store_users_role` on (`store_id`, `role`) - Listing users by role within a store

---

### store_settings

Single-row-per-store JSON settings bag.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PRIMARY KEY, FK -> stores(id) ON DELETE CASCADE | One-to-one with stores |
| settings_json | TEXT | No | '{}' | - | JSON settings object |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Notes:** No additional indexes needed - primary key on `store_id` covers all queries.

---

## Epic 2 - Catalog, Collections, Inventory, and Media

### products

Core product record, scoped to a store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| title | TEXT | No | - | - | Product display title |
| handle | TEXT | No | - | UNIQUE per (store_id, handle) | URL-friendly slug |
| status | TEXT | No | 'draft' | CHECK IN ('draft', 'active', 'archived') | |
| description_html | TEXT | Yes | NULL | - | Rich-text description |
| vendor | TEXT | Yes | NULL | - | Manufacturer/brand |
| product_type | TEXT | Yes | NULL | - | Category classification |
| tags | TEXT | No | '[]' | - | JSON array, e.g. ["summer","sale"] |
| published_at | TEXT | Yes | NULL | - | ISO-8601 timestamp; null = unpublished |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_products_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store for URL routing
- `idx_products_store_id` on (`store_id`) - FK lookup, listing all products in a store
- `idx_products_store_status` on (`store_id`, `status`) - Filtering products by status within a store
- `idx_products_published_at` on (`store_id`, `published_at`) - Sorting/filtering by publication date
- `idx_products_vendor` on (`store_id`, `vendor`) - Filtering products by vendor
- `idx_products_product_type` on (`store_id`, `product_type`) - Filtering products by type

**Notes:** `tags` is a JSON-encoded array (e.g. `["summer","sale"]`). The `handle` is unique per store, not globally.

---

### product_options

Named option dimensions for a product (e.g. Size, Color).

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| product_id | INTEGER | No | - | FK -> products(id) ON DELETE CASCADE | Parent product |
| name | TEXT | No | - | - | Option name, e.g. "Size", "Color" |
| position | INTEGER | No | 0 | UNIQUE per (product_id, position) | Display ordering |

**Indexes:**

- `idx_product_options_product_id` on (`product_id`) - FK lookup, listing options per product
- `idx_product_options_product_position` UNIQUE on (`product_id`, `position`) - Enforces unique ordering per product

---

### product_option_values

Individual values within a product option (e.g. "Small", "Medium", "Large").

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| product_option_id | INTEGER | No | - | FK -> product_options(id) ON DELETE CASCADE | Parent option |
| value | TEXT | No | - | - | Display value, e.g. "Small" |
| position | INTEGER | No | 0 | UNIQUE per (product_option_id, position) | Display ordering |

**Indexes:**

- `idx_product_option_values_option_id` on (`product_option_id`) - FK lookup, listing values per option
- `idx_product_option_values_option_position` UNIQUE on (`product_option_id`, `position`) - Enforces unique ordering per option

---

### product_variants

Purchasable SKU-level variant of a product.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| product_id | INTEGER | No | - | FK -> products(id) ON DELETE CASCADE | Parent product |
| sku | TEXT | Yes | NULL | - | Stock keeping unit |
| barcode | TEXT | Yes | NULL | - | UPC/EAN barcode |
| price_amount | INTEGER | No | 0 | - | Price in minor units (cents) |
| compare_at_amount | INTEGER | Yes | NULL | - | Original/compare price in minor units |
| currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
| weight_g | INTEGER | Yes | NULL | - | Weight in grams |
| requires_shipping | INTEGER | No | 1 | - | Boolean: physical product flag |
| is_default | INTEGER | No | 0 | - | Boolean: default variant flag |
| position | INTEGER | No | 0 | - | Display ordering |
| status | TEXT | No | 'active' | CHECK IN ('active', 'archived') | |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_product_variants_product_id` on (`product_id`) - FK lookup, listing variants per product
- `idx_product_variants_sku` on (`sku`) - SKU lookup across all variants
- `idx_product_variants_barcode` on (`barcode`) - Barcode scanning lookup
- `idx_product_variants_product_position` on (`product_id`, `position`) - Ordered variant listing
- `idx_product_variants_product_default` on (`product_id`, `is_default`) - Quick lookup of default variant

**Notes:** `price_amount` and `compare_at_amount` are in minor currency units (cents). A product always has at least one variant (the default).

---

### variant_option_values

Maps a variant to its chosen option values (e.g. variant #5 = Size:Large + Color:Red). Junction table with composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| variant_id | INTEGER | No | - | PK (composite), FK -> product_variants(id) ON DELETE CASCADE | |
| product_option_value_id | INTEGER | No | - | PK (composite), FK -> product_option_values(id) ON DELETE CASCADE | |

**Primary Key:** (`variant_id`, `product_option_value_id`)

**Indexes:**

- `idx_variant_option_values_value_id` on (`product_option_value_id`) - Reverse lookup: find all variants with a given option value

**Notes:** This is a junction table that links variants to the specific option values they represent. Required to resolve which combination of options each variant corresponds to.

---

### inventory_items

Stock tracking per variant per store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| variant_id | INTEGER | No | - | FK -> product_variants(id) ON DELETE CASCADE, UNIQUE | One inventory record per variant |
| quantity_on_hand | INTEGER | No | 0 | - | Total physical stock |
| quantity_reserved | INTEGER | No | 0 | - | Stock allocated to open orders |
| policy | TEXT | No | 'deny' | CHECK IN ('deny', 'continue') | Oversell policy |

**Indexes:**

- `idx_inventory_items_variant_id` UNIQUE on (`variant_id`) - Enforces one inventory record per variant, fast variant lookup
- `idx_inventory_items_store_id` on (`store_id`) - Listing all inventory for a store

**Notes:** `quantity_on_hand` is the total physical stock. `quantity_reserved` is stock allocated to open orders not yet fulfilled. Available = on_hand - reserved. Policy `deny` prevents overselling; `continue` allows selling below zero.

---

### collections

Curated or rule-based product groupings.


     1→# API and Routing Specification
     2→
     3→This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.
     4→
     5→All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).
     6→
     7→---
     8→
     9→## 1. Web Routes (Blade/Livewire Pages)
    10→
    11→### 1.1 Admin Auth Routes
    12→
    13→No middleware beyond the default `web` group. These routes handle admin login and logout.
    14→
    15→| Method | Path | Description | Component / Action |
    16→|--------|------|-------------|--------------------|
    17→| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
    18→| POST | `/admin/login` | Process admin login | Handled by Livewire action |
    19→| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
    20→| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
    21→| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
    22→| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
    23→| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |
    24→
    25→Rate limit: `login` tier (5 per minute per IP).
    26→
    27→---
    28→
    29→### 1.2 Admin Routes
    30→
    31→**Prefix:** `/admin`
    32→**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`
    33→
    34→All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).
    35→
    36→> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.
    37→
    38→#### Dashboard
    39→
    40→| Method | Path | Description | Component |
    41→|--------|------|-------------|-----------|
    42→| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |
    43→
    44→#### Products
    45→
    46→| Method | Path | Description | Component |
    47→|--------|------|-------------|-----------|
    48→| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
    49→| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
    50→| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |
    51→
    52→#### Inventory
    53→
    54→| Method | Path | Description | Component |
    55→|--------|------|-------------|-----------|
    56→| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |
    57→
    58→#### Collections
    59→
    60→| Method | Path | Description | Component |
    61→|--------|------|-------------|-----------|
    62→| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
    63→| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
    64→| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |
    65→
    66→#### Orders
    67→
    68→| Method | Path | Description | Component |
    69→|--------|------|-------------|-----------|
    70→| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
    71→| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |
    72→
    73→#### Customers
    74→
    75→| Method | Path | Description | Component |
    76→|--------|------|-------------|-----------|
    77→| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
    78→| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |
    79→
    80→#### Discounts
    81→
    82→| Method | Path | Description | Component |
    83→|--------|------|-------------|-----------|
    84→| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
    85→| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
    86→| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |
    87→
    88→#### Settings
    89→
    90→| Method | Path | Description | Component |
    91→|--------|------|-------------|-----------|
    92→| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
    93→| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
    94→| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |
    95→
    96→#### Themes and Online Store
    97→
    98→| Method | Path | Description | Component |
    99→|--------|------|-------------|-----------|
   100→| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
   101→| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |
   102→
   103→#### Pages
   104→
   105→| Method | Path | Description | Component |
   106→|--------|------|-------------|-----------|
   107→| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
   108→| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
   109→| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |
   110→
   111→#### Navigation
   112→
   113→| Method | Path | Description | Component |
   114→|--------|------|-------------|-----------|
   115→| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |
   116→
   117→#### Apps
   118→
   119→| Method | Path | Description | Component |
   120→|--------|------|-------------|-----------|
   121→| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
   122→| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |
   123→
   124→#### Developers
   125→
   126→| Method | Path | Description | Component |
   127→|--------|------|-------------|-----------|
   128→| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |
   129→
   130→> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.
   131→
   132→#### Analytics
   133→
   134→| Method | Path | Description | Component |
   135→|--------|------|-------------|-----------|
   136→| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |
   137→
   138→#### Search Settings
   139→
   140→| Method | Path | Description | Component |
   141→|--------|------|-------------|-----------|
   142→| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |
   143→
   144→---
   145→
   146→### 1.3 Storefront Routes
   147→
   148→**Middleware:** `web`, `store.resolve`
   149→
   150→These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.
   151→
   152→| Method | Path | Description | Component / View |
   153→|--------|------|-------------|------------------|
   154→| GET | `/` | Home page | `Storefront\Home` |
   155→| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
   156→| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
   157→| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
   158→| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
   159→| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
   160→| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |
   161→
   162→#### Customer Account Routes
   163→
   164→**Additional middleware:** `auth:customer`
   165→
   166→| Method | Path | Description | Component |
   167→|--------|------|-------------|-----------|
   168→| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
   169→| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
   170→| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
   171→| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |
   172→
   173→#### Customer Auth Routes (no auth required)
   174→
   175→| Method | Path | Description | Component |
   176→|--------|------|-------------|-----------|
   177→| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
   178→| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
   179→| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
   180→| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
   181→| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
   182→| POST | `/reset-password` | Reset password | Handled by Livewire action |
   183→
   184→---
   185→
   186→### 1.4 Checkout Routes
   187→
   188→**Middleware:** `web`, `store.resolve`
   189→
   190→| Method | Path | Description | Component |
   191→|--------|------|-------------|-----------|
   192→| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
   193→| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |
   194→
   195→---
   196→
   197→## 2. Storefront REST API
   198→
   199→**Prefix:** `/api/storefront/v1`
   200→**Middleware:** `store.resolve`, `throttle:api.storefront`
   201→**Content-Type:** `application/json`
   202→
   203→These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).
   204→
   205→---
   206→
   207→### 2.1 Cart Endpoints
   208→
   209→#### POST /api/storefront/v1/carts
   210→
   211→Create a new cart.
   212→
   213→- **Authentication:** None (guest) or session-based (logged-in customer)
   214→- **Rate limit:** `api.storefront` (120/min per IP)
   215→
   216→**Request body:**
   217→
   218→```json
   219→{
   220→  "currency": "EUR"
   221→}
   222→```
   223→
   224→| Field | Type | Required | Validation |
   225→|-------|------|----------|------------|
   226→| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |
   227→
   228→**Success response:** `201 Created`
   229→
   230→```json
   231→{
   232→  "id": 42,
   233→  "store_id": 1,
   234→  "customer_id": null,
   235→  "currency": "EUR",
   236→  "cart_version": 1,
   237→  "status": "active",
   238→  "lines": [],
   239→  "totals": {
   240→    "subtotal": 0,
   241→    "discount": 0,
   242→    "total": 0,
   243→    "currency": "EUR"
   244→  },
   245→  "created_at": "2026-02-11T10:00:00Z",
   246→  "updated_at": "2026-02-11T10:00:00Z"
   247→}
   248→```
   249→
   250→**Error responses:**
   251→- `422` - Validation error (invalid currency)
   252→
   253→---
   254→
   255→#### GET /api/storefront/v1/carts/{cartId}
   256→
   257→Retrieve a cart with all lines and computed totals.
   258→
   259→- **Authentication:** None (cart ID acts as token)
   260→- **Rate limit:** `api.storefront` (120/min per IP)
   261→
   262→**URL parameters:**
   263→
   264→| Parameter | Type | Required | Description |
   265→|-----------|------|----------|-------------|
   266→| `cartId` | integer | Yes | The cart identifier |
   267→
   268→**Success response:** `200 OK`
   269→
   270→```json
   271→{
   272→  "id": 42,
   273→  "store_id": 1,
   274→  "customer_id": null,
   275→  "currency": "EUR",
   276→  "cart_version": 3,
   277→  "status": "active",
   278→  "lines": [
   279→    {
   280→      "id": 201,
   281→      "variant_id": 101,
   282→      "product_title": "Classic T-Shirt",
   283→      "variant_title": "Blue / Medium",
   284→      "sku": "TSH-BLU-M",
   285→      "quantity": 2,
   286→      "unit_price_amount": 2500,
   287→      "line_subtotal_amount": 5000,
   288→      "line_discount_amount": 0,
   289→      "line_total_amount": 5000,
   290→      "image_url": "https://cdn.example.com/products/tshirt-blue.jpg",
   291→      "requires_shipping": true,
   292→      "available_quantity": 50
   293→    }
   294→  ],
   295→  "totals": {
   296→    "subtotal": 5000,
   297→    "discount": 0,
   298→    "total": 5000,
   299→    "currency": "EUR",
   300→    "line_count": 1,

# API and Routing Specification

This document defines every route, endpoint, middleware stack, request/response schema, and rate limit for the e-commerce platform. The admin panel is built with Livewire v4 + Flux UI (server-rendered), while the storefront uses Blade + Livewire. REST API endpoints serve cart/checkout operations and programmatic admin access.

All monetary amounts are stored and transmitted in **minor units** (cents). All IDs are auto-incrementing integers. All monetary amounts are in minor units (cents).

---

## 1. Web Routes (Blade/Livewire Pages)

### 1.1 Admin Auth Routes

No middleware beyond the default `web` group. These routes handle admin login and logout.

| Method | Path | Description | Component / Action |
|--------|------|-------------|--------------------|
| GET | `/admin/login` | Admin login page | `Livewire: Admin\Auth\Login` |
| POST | `/admin/login` | Process admin login | Handled by Livewire action |
| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
| GET | `/admin/forgot-password` | Show forgot password form | `Livewire: Admin\Auth\ForgotPassword` |
| POST | `/admin/forgot-password` | Send password reset link | Handled by Livewire action |
| GET | `/admin/reset-password/{token}` | Show reset password form | `Livewire: Admin\Auth\ResetPassword` |
| POST | `/admin/reset-password` | Reset password with token | Handled by Livewire action |

Rate limit: `login` tier (5 per minute per IP).

---

### 1.2 Admin Routes

**Prefix:** `/admin`
**Middleware:** `web`, `auth`, `verified`, `store.resolve`, `role.check`

All admin pages render Livewire full-page components. Data mutations happen via Livewire actions (not separate POST endpoints).

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |

#### Products

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |

#### Inventory

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |

#### Collections

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |

#### Orders

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |

#### Customers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |

#### Discounts

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |

#### Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |

#### Themes and Online Store

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |

#### Pages

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |

#### Navigation

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |

#### Search Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |

---

### 1.3 Storefront Routes

**Middleware:** `web`, `store.resolve`

These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |

---

## 2. Storefront REST API

**Prefix:** `/api/storefront/v1`
**Middleware:** `store.resolve`, `throttle:api.storefront`
**Content-Type:** `application/json`

These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).

---

### 2.1 Cart Endpoints

#### POST /api/storefront/v1/carts

Create a new cart.

- **Authentication:** None (guest) or session-based (logged-in customer)
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "currency": "EUR"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `currency` | string | No | Valid ISO 4217 code. Defaults to store's `default_currency`. |

**Success response:** `201 Created`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 1,
  "status": "active",
  "lines": [],
  "totals": {
    "subtotal": 0,
    "discount": 0,
    "total": 0,
    "currency": "EUR"
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:00:00Z"
}
```

**Error responses:**
- `422` - Validation error (invalid currency)

---

#### GET /api/storefront/v1/carts/{cartId}

Retrieve a cart with all lines and computed totals.

- **Authentication:** None (cart ID acts as token)
- **Rate limit:** `api.storefront` (120/min per IP)

**URL parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cartId` | integer | Yes | The cart identifier |

**Success response:** `200 OK`

```json
{
  "id": 42,
  "store_id": 1,
  "customer_id": null,
  "currency": "EUR",
  "cart_version": 3,
  "status": "active",
  "lines": [
    {
      "id": 201,
      "variant_id": 101,
      "product_title": "Classic T-Shirt",
      "variant_title": "Blue / Medium",
      "sku": "TSH-BLU-M",
      "quantity": 2,
      "unit_price_amount": 2500,
      "line_subtotal_amount": 5000,
      "line_discount_amount": 0,
      "line_total_amount": 5000,
      "image_url": "https://cdn.example.com/products/tshirt-blue.jpg",
      "requires_shipping": true,
      "available_quantity": 50
    }
  ],
  "totals": {
    "subtotal": 5000,
    "discount": 0,
    "total": 5000,
    "currency": "EUR",
    "line_count": 1,


These specs are very detailed. Let me read the rest in parallel to understand the full scope, then I'll plan and execute the phases.


Let me continue reading the specs and understand the full scope.