content:
   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 |
   501→| `addLine(Cart, int $variantId, int $qty): CartLine` | Validates variant/product active, inventory, increments existing line or creates new |
   502→| `updateLineQuantity(Cart, int $lineId, int $qty): CartLine` | Validates quantity, recalculates amounts |
   503→| `removeLine(Cart, int $lineId): void` | Deletes line, increments version |
   504→| `getOrCreateForSession(Store, ?Customer): Cart` | Session-based cart binding |
   505→| `mergeOnLogin(Cart $guest, Cart $customer): Cart` | Merges guest cart into customer cart |
   506→
   507→All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.
   508→
   509→#### Step 4.4: Discount Service
   510→
   511→**File to create:** `App\Services\DiscountService`
   512→
   513→| Method | Description |
   514→|--------|-------------|
   515→| `validate(string $code, Store $store, Cart $cart): Discount` | Validates code exists, is active, dates valid, usage limit not reached, minimum purchase met |
   516→| `calculate(Discount $discount, int $subtotal, array $lines): DiscountResult` | Calculates amount, allocates proportionally across qualifying lines |
   517→
   518→Case-insensitive code lookup. Throws `InvalidDiscountException` with specific reason codes.
   519→
   520→#### Step 4.5: Shipping Calculator
   521→
   522→**File to create:** `App\Services\ShippingCalculator`
   523→
   524→| Method | Description |
   525→|--------|-------------|
   526→| `getAvailableRates(Store $store, array $address): Collection` | Matches zones by country/region, returns active rates |
   527→| `calculate(ShippingRate $rate, Cart $cart): int` | Calculates cost based on rate type (flat/weight/price) |
   528→
   529→Zone matching: checks `countries_json` for country code, then `regions_json` for region code.
   530→
   531→#### Step 4.6: Tax Calculator
   532→
   533→**File to create:** `App\Services\TaxCalculator`
   534→
   535→| Method | Description |
   536→|--------|-------------|
   537→| `calculate(int $amount, TaxSettings $settings, array $address): TaxResult` | Calculates tax per manual rate or provider |
   538→| `extractInclusive(int $grossAmount, int $rateBasisPoints): int` | Extracts tax from gross (prices-include-tax mode) |
   539→| `addExclusive(int $netAmount, int $rateBasisPoints): int` | Adds tax to net amount |
   540→
   541→All math uses integers. Rates stored in basis points (1900 = 19%).
   542→
   543→#### Step 4.7: Pricing Engine
   544→
   545→**File to create:** `App\Services\PricingEngine`
   546→
   547→Method: `calculate(Checkout $checkout): PricingResult`
   548→
   549→**Pipeline steps:**
   550→1. Line subtotals: `unit_price * quantity` per line
   551→2. Cart subtotal: sum of line subtotals
   552→3. Discount: apply rules, allocate proportionally
   553→4. Discounted subtotal: subtotal minus discount
   554→5. Shipping: based on selected method/rate
   555→6. Tax: on discounted amounts (plus shipping if taxable)
   556→7. Total: discounted_subtotal + shipping + tax_total
   557→
   558→Result stored in `checkouts.totals_json` as snapshot.
   559→
   560→**Value objects to create:**
   561→
   562→| Type | Name | Fields |
   563→|------|------|--------|
   564→| Value Object | `App\ValueObjects\PricingResult` | subtotal, discount, shipping, taxLines, taxTotal, total, currency |
   565→| Value Object | `App\ValueObjects\TaxLine` | name, rate (basis points), amount (cents) |
   566→
   567→#### Step 4.8: Checkout State Machine
   568→
   569→**File to create:** `App\Services\CheckoutService`
   570→
   571→| Transition | Method | Requirements |
   572→|------------|--------|-------------|
   573→| `started` -> `addressed` | `setAddress(Checkout, array)` | Valid email, shipping address fields |
   574→| `addressed` -> `shipping_selected` | `setShippingMethod(Checkout, int $rateId)` | Rate belongs to applicable zone |
   575→| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
   576→| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
   577→| Any active -> `expired` | `expireCheckout(Checkout)` | Via scheduled job |
   578→
   579→**Files to create:**
   580→
   581→| Type | Name | Purpose |
   582→|------|------|---------|
   583→| Job | `App\Jobs\ExpireAbandonedCheckouts` | Runs every 15 minutes via `routes/console.php`; finds checkouts with `expires_at < now()` and active status; releases reserved inventory, transitions to `expired` |
   584→| Job | `App\Jobs\CleanupAbandonedCarts` | Runs daily; marks carts with `status = active` and `updated_at < 14 days ago` as `abandoned` |
   585→
   586→#### Step 4.9: Storefront Cart/Checkout UI
   587→
   588→**Files to create:**
   589→
   590→| Component | Description |
   591→|-----------|-------------|
   592→| `Storefront\CartDrawer` | Slide-out panel: line items, quantity controls, discount code input, subtotal, checkout button |
   593→| `Storefront\Cart\Show` | Full cart page with same functionality plus shipping estimate |
   594→| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
   595→| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |
   596→
   597→---
   598→
   599→### Phase 5: Payments, Orders, Fulfillment
   600→
   601→**Priority:** HIGH - completes the purchase flow.
   602→
   603→#### Step 5.1: Migrations (Batch 5-7)
   604→
   605→1. `create_customers_table` - FK: `store_id`
   606→2. `create_customer_addresses_table` - FK: `customer_id`
   607→3. `create_orders_table` - FK: `store_id`, `customer_id`
   608→4. `create_order_lines_table` - FK: `order_id`, `product_id` (nullable), `variant_id` (nullable)
   609→5. `create_payments_table` - FK: `order_id`
   610→6. `create_refunds_table` - FK: `order_id`, `payment_id`
   611→7. `create_fulfillments_table` - FK: `order_id`
   612→8. `create_fulfillment_lines_table` - FK: `fulfillment_id`, `order_line_id`
   613→
   614→#### Step 5.2: Models
   615→
   616→**Files to create:**
   617→
   618→| Type | Name | Purpose |
   619→|------|------|---------|
   620→| Model (with migration, factory, seeder) | `Customer` | Store customer entity |
   621→| Model (with migration, factory, seeder) | `CustomerAddress` | Customer address |
   622→| Model (with migration, factory, seeder) | `Order` | Order entity |
   623→| Model (with migration, factory, seeder) | `OrderLine` | Order line item |
   624→| Model (with migration, factory, seeder) | `Payment` | Payment record |
   625→| Model (with migration, factory, seeder) | `Refund` | Refund record |
   626→| Model (with migration, factory, seeder) | `Fulfillment` | Fulfillment record |
   627→| Model (with migration, factory, seeder) | `FulfillmentLine` | Fulfillment line item |
   628→
   629→**Relationship definitions:**
   630→
   631→| Model | Key Relationships |
   632→|-------|-------------------|
   633→| `Customer` | `belongsTo(Store)`, `hasMany(CustomerAddress)`, `hasMany(Order)`, `hasMany(Cart)` |
   634→| `CustomerAddress` | `belongsTo(Customer)` |
   635→| `Order` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(OrderLine)`, `hasMany(Payment)`, `hasMany(Refund)`, `hasMany(Fulfillment)` |
   636→| `OrderLine` | `belongsTo(Order)`, `belongsTo(Product)` (nullable), `belongsTo(ProductVariant)` (nullable) |
   637→| `Payment` | `belongsTo(Order)` |
   638→| `Refund` | `belongsTo(Order)`, `belongsTo(Payment)` |
   639→| `Fulfillment` | `belongsTo(Order)`, `hasMany(FulfillmentLine)` |
   640→| `FulfillmentLine` | `belongsTo(Fulfillment)`, `belongsTo(OrderLine)` |
   641→
   642→**Enums to create:**
   643→
   644→| Enum | Values |
   645→|------|--------|
   646→| `OrderStatus` | `Pending`, `Paid`, `Fulfilled`, `Cancelled`, `Refunded` |
   647→| `FinancialStatus` | `Pending`, `Authorized`, `Paid`, `PartiallyRefunded`, `Refunded`, `Voided` |
   648→| `FulfillmentStatus` | `Unfulfilled`, `Partial`, `Fulfilled` |
   649→| `PaymentMethod` | `CreditCard`, `Paypal`, `BankTransfer` |
   650→| `PaymentStatus` | `Pending`, `Captured`, `Failed`, `Refunded` |
   651→| `RefundStatus` | `Pending`, `Processed`, `Failed` |
   652→| `FulfillmentShipmentStatus` | `Pending`, `Shipped`, `Delivered` |
   653→
   654→#### Step 5.3: Payment Service (Mock PSP)
   655→
   656→**Files to create:**
   657→
   658→| Type | Name | Purpose |
   659→|------|------|---------|
   660→| Enum | `App\Enums\PaymentMethod` | CreditCard, Paypal, BankTransfer |
   661→| Interface | `App\Contracts\PaymentProvider` | Defines charge, refund methods |
   662→| Service | `App\Services\Payments\MockPaymentProvider` | Implements PaymentProvider with in-process mock logic |
   663→
   664→**PaymentProvider interface methods:**
   665→
   666→| Method | Description |
   667→|--------|-------------|
   668→| `charge(Checkout $checkout, PaymentMethod $method, array $details): PaymentResult` | Processes payment in-process. For credit_card: validates magic card numbers, returns success/decline. For paypal: always succeeds. For bank_transfer: returns pending (deferred capture). |
   669→| `refund(Payment $payment, int $amount): RefundResult` | Issues a mock refund (always succeeds). |
   670→
   671→**MockPaymentProvider requirements:**
   672→- No external API calls, no webhooks, no redirects
   673→- Magic card numbers: `4242424242424242` (success), `4000000000000002` (decline), `4000000000009995` (insufficient funds)
   674→- Credit card / PayPal: instant capture, returns PaymentResult with status=captured
   675→- Bank transfer: deferred capture, returns PaymentResult with status=pending
   676→- Generates mock reference IDs (`mock_` + random string)
   677→
   678→**Registration:** Bind `PaymentProvider` interface to `MockPaymentProvider` in `AppServiceProvider`.
   679→
   680→#### Step 5.3b: Admin Payment Confirmation
   681→
   682→**Files to create:**
   683→
   684→| Type | Name | Purpose |
   685→|------|------|---------|
   686→| Job | `App\Jobs\CancelUnpaidBankTransferOrders` | Runs daily via `routes/console.php`; finds bank_transfer orders with `financial_status = pending` and `placed_at < now() - config days`; cancels them and releases reserved inventory |
   687→
   688→**Admin "Confirm Payment" action** (in OrderService or dedicated action class):
   689→1. Validate order has `payment_method = bank_transfer` and `financial_status = pending`
   690→2. Update `financial_status` to `paid`, `status` to `paid`
   691→3. Update payment record status to `captured`
   692→4. Commit reserved inventory
   693→5. If all items are digital (`requires_shipping = false`), auto-create a delivered fulfillment
   694→6. Dispatch `OrderPaid` event
   695→
   696→#### Step 5.4: Order Service
   697→
   698→**File to create:** `App\Services\OrderService`
   699→

content:
| `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 |
| `addLine(Cart, int $variantId, int $qty): CartLine` | Validates variant/product active, inventory, increments existing line or creates new |
| `updateLineQuantity(Cart, int $lineId, int $qty): CartLine` | Validates quantity, recalculates amounts |
| `removeLine(Cart, int $lineId): void` | Deletes line, increments version |
| `getOrCreateForSession(Store, ?Customer): Cart` | Session-based cart binding |
| `mergeOnLogin(Cart $guest, Cart $customer): Cart` | Merges guest cart into customer cart |

All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.

#### Step 4.4: Discount Service

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

| Method | Description |
|--------|-------------|
| `validate(string $code, Store $store, Cart $cart): Discount` | Validates code exists, is active, dates valid, usage limit not reached, minimum purchase met |
| `calculate(Discount $discount, int $subtotal, array $lines): DiscountResult` | Calculates amount, allocates proportionally across qualifying lines |

Case-insensitive code lookup. Throws `InvalidDiscountException` with specific reason codes.

#### Step 4.5: Shipping Calculator

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

| Method | Description |
|--------|-------------|
| `getAvailableRates(Store $store, array $address): Collection` | Matches zones by country/region, returns active rates |
| `calculate(ShippingRate $rate, Cart $cart): int` | Calculates cost based on rate type (flat/weight/price) |

Zone matching: checks `countries_json` for country code, then `regions_json` for region code.

#### Step 4.6: Tax Calculator

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

| Method | Description |
|--------|-------------|
| `calculate(int $amount, TaxSettings $settings, array $address): TaxResult` | Calculates tax per manual rate or provider |
| `extractInclusive(int $grossAmount, int $rateBasisPoints): int` | Extracts tax from gross (prices-include-tax mode) |
| `addExclusive(int $netAmount, int $rateBasisPoints): int` | Adds tax to net amount |

All math uses integers. Rates stored in basis points (1900 = 19%).

#### Step 4.7: Pricing Engine

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

Method: `calculate(Checkout $checkout): PricingResult`

**Pipeline steps:**
1. Line subtotals: `unit_price * quantity` per line
2. Cart subtotal: sum of line subtotals
3. Discount: apply rules, allocate proportionally
4. Discounted subtotal: subtotal minus discount
5. Shipping: based on selected method/rate
6. Tax: on discounted amounts (plus shipping if taxable)
7. Total: discounted_subtotal + shipping + tax_total

Result stored in `checkouts.totals_json` as snapshot.

**Value objects to create:**

| Type | Name | Fields |
|------|------|--------|
| Value Object | `App\ValueObjects\PricingResult` | subtotal, discount, shipping, taxLines, taxTotal, total, currency |
| Value Object | `App\ValueObjects\TaxLine` | name, rate (basis points), amount (cents) |

#### Step 4.8: Checkout State Machine

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

| Transition | Method | Requirements |
|------------|--------|-------------|
| `started` -> `addressed` | `setAddress(Checkout, array)` | Valid email, shipping address fields |
| `addressed` -> `shipping_selected` | `setShippingMethod(Checkout, int $rateId)` | Rate belongs to applicable zone |
| `shipping_selected` -> `payment_selected` | `selectPaymentMethod(Checkout, string)` | All data valid, reserve inventory |
| `payment_selected` -> `completed` | `completeCheckout(Checkout, array)` | Payment confirmed, idempotent |
| Any active -> `expired` | `expireCheckout(Checkout)` | Via scheduled job |

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Job | `App\Jobs\ExpireAbandonedCheckouts` | Runs every 15 minutes via `routes/console.php`; finds checkouts with `expires_at < now()` and active status; releases reserved inventory, transitions to `expired` |
| Job | `App\Jobs\CleanupAbandonedCarts` | Runs daily; marks carts with `status = active` and `updated_at < 14 days ago` as `abandoned` |

#### Step 4.9: Storefront Cart/Checkout UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\CartDrawer` | Slide-out panel: line items, quantity controls, discount code input, subtotal, checkout button |
| `Storefront\Cart\Show` | Full cart page with same functionality plus shipping estimate |
| `Storefront\Checkout\Show` | Multi-step: Contact/Address -> Shipping method -> Payment. Stepper UI. |
| `Storefront\Checkout\Confirmation` | Order confirmation: order number, items, totals, next steps |

---

### Phase 5: Payments, Orders, Fulfillment

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

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

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

#### Step 5.2: Models

**Files to create:**

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

**Relationship definitions:**

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

**Enums to create:**

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

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

**Files to create:**

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

**PaymentProvider interface methods:**

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

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

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

#### Step 5.3b: Admin Payment Confirmation

**Files to create:**

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

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

#### Step 5.4: Order Service

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


teamName: wild-sleeping-shell

userType: external

slug: iterative-seeking-pine

type: message

role: assistant

service_tier: standard

type: assistant


type: progress

hookEvent: PostToolUse

command: callback


type: user

role: user

content:
   700→| Method | Description |
   701→|--------|-------------|
   702→| `createFromCheckout(Checkout $checkout): Order` | Atomic transaction: create order + order lines with snapshots, commit inventory, mark cart converted, dispatch `OrderCreated` event |
   703→| `generateOrderNumber(Store $store): string` | Sequential per store: #1001, #1002, etc. |
   704→| `cancel(Order $order, string $reason): void` | Only if not yet fulfilled, releases inventory |
   705→
   706→Order lines store snapshots (`title_snapshot`, `sku_snapshot`) so order history survives product deletion.
   707→
   708→#### Step 5.5: Refund Service
   709→
   710→**File to create:** `App\Services\RefundService`
   711→
   712→| Method | Description |
   713→|--------|-------------|
   714→| `create(Order $order, Payment $payment, int $amount, ?string $reason, bool $restock): Refund` | Validates amount does not exceed payment, calls provider refund API, updates financial status |
   715→
   716→- Partial refunds: `financial_status` -> `partially_refunded`
   717→- Full refunds: `financial_status` -> `refunded`
   718→- Restock flag: calls `InventoryService::restock()` for each line
   719→
   720→#### Step 5.6: Fulfillment Service
   721→
   722→**File to create:** `App\Services\FulfillmentService`
   723→
   724→| Method | Description |
   725→|--------|-------------|
   726→| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
   727→| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
   728→| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |
   729→
   730→Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).
   731→
   732→**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).
   733→
   734→**Events to create:**
   735→
   736→| Type | Name | Purpose |
   737→|------|------|---------|
   738→| Event | `OrderCreated` | Dispatched when order is created from checkout |
   739→| Event | `OrderPaid` | Dispatched when payment is captured |
   740→| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
   741→| Event | `OrderCancelled` | Dispatched when order is cancelled |
   742→| Event | `OrderRefunded` | Dispatched when refund is processed |
   743→
   744→---
   745→
   746→### Phase 6: Customer Accounts
   747→
   748→**Priority:** MEDIUM - enhances the shopping experience.
   749→
   750→#### Step 6.1: Customer Auth
   751→
   752→- `Customer` model with custom `customer` guard (already configured in Phase 1)
   753→- Custom `CustomerUserProvider` - scopes queries by `store_id`
   754→- Customer email unique per store, not globally
   755→
   756→#### Step 6.2: Customer Account Pages (Livewire)
   757→
   758→**Files to create:**
   759→
   760→| Component | Route | Description |
   761→|-----------|-------|-------------|
   762→| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
   763→| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
   764→| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
   765→| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
   766→| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
   767→| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |
   768→
   769→Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
   770→
   771→---
   772→
   773→### Phase 7: Admin Panel
   774→
   775→**Priority:** MEDIUM - merchant management interface.
   776→
   777→#### Step 7.1: Admin Layout
   778→
   779→**File to create:** `resources/views/livewire/admin/layout/app.blade.php`
   780→
   781→**Components to create:**
   782→
   783→| Component | Description |
   784→|-----------|-------------|
   785→| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
   786→| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
   787→| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |
   788→
   789→Toast notifications via Livewire events.
   790→
   791→#### Step 7.2: Dashboard
   792→
   793→**File to create:** `Admin\Dashboard` Livewire component
   794→
   795→Requirements:
   796→- KPI tiles: total sales (period), order count, average order value, conversion rate
   797→- Orders chart (last 30 days line chart)
   798→- Recent orders table (last 10)
   799→- Date range filtering via Livewire properties
   800→
   801→#### Step 7.3: Product Management
   802→
   803→**Files to create:**
   804→
   805→| Component | Description |
   806→|-----------|-------------|
   807→| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
   808→| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |
   809→
   810→> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.
   811→
   812→Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.
   813→
   814→Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
   815→
   816→#### Step 7.4: Order Management
   817→
   818→**Files to create:**
   819→
   820→| Component | Description |
   821→|-----------|-------------|
   822→| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
   823→| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |
   824→
   825→Fulfillment modal: select lines and quantities, add tracking info, submit.
   826→Refund modal: enter amount (partial or full), reason, restock checkbox, submit.
   827→
   828→#### Step 7.5: Other Admin Sections
   829→
   830→**Files to create:**
   831→
   832→| Component | Description |
   833→|-----------|-------------|
   834→| `Admin\Collections\Index` | Collection list |
   835→| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
   836→| `Admin\Customers\Index` | Customer list with search |
   837→| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
   838→| `Admin\Discounts\Index` | Discount list with status/type filters |
   839→| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
   840→| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
   841→| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
   842→| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
   843→| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
   844→| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
   845→| `Admin\Pages\Index` | Content pages list |
   846→| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
   847→| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
   848→| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
   849→| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
   850→| `Admin\Apps\Index` | App directory |
   851→| `Admin\Apps\Show` | Installed app detail |
   852→| `Admin\Developers\Index` | API token management, webhook subscriptions |
   853→
   854→---
   855→
   856→### Phase 8: Search
   857→
   858→**Priority:** LOW - enhances product discovery.
   859→
   860→#### Step 8.1: Migrations
   861→
   862→1. `create_search_settings_table` - PK: `store_id`
   863→2. `create_search_queries_table` - FK: `store_id`
   864→3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table
   865→
   866→#### Step 8.2: Search Service
   867→
   868→**File to create:** `App\Services\SearchService`
   869→
   870→| Method | Description |
   871→|--------|-------------|
   872→| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
   873→| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
   874→| `syncProduct(Product $product): void` | Upsert into FTS5 index |
   875→| `removeProduct(int $productId): void` | Delete from FTS5 index |
   876→
   877→**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.
   878→
   879→#### Step 8.3: Search UI
   880→
   881→**Files to create:**
   882→
   883→| Component | Description |
   884→|-----------|-------------|
   885→| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
   886→| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |
   887→
   888→---
   889→
   890→### Phase 9: Analytics
   891→
   892→**Priority:** LOW - reporting and insights.
   893→
   894→#### Step 9.1: Migrations
   895→
   896→1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
   897→2. `create_analytics_daily_table` - composite PK: `store_id` + `date`
   898→
   899→#### Step 9.2: Analytics Service
   900→
   901→**File to create:** `App\Services\AnalyticsService`
   902→
   903→| Method | Description |
   904→|--------|-------------|
   905→| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
   906→| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |
   907→
   908→Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.
   909→
   910→**File to create:** `App\Jobs\AggregateAnalytics`
   911→
   912→Requirements:
   913→- Runs daily via `routes/console.php`
   914→- Aggregates raw events into `analytics_daily` rows
   915→- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count
   916→
   917→---
   918→
   919→### Phase 10: Apps and Webhooks
   920→
   921→**Priority:** LOW - extensibility.
   922→
   923→#### Step 10.1: Migrations
   924→
   925→1. `create_apps_table`
   926→2. `create_app_installations_table` - FK: `store_id`, `app_id`
   927→3. `create_oauth_clients_table` - FK: `app_id`
   928→4. `create_oauth_tokens_table` - FK: `installation_id`
   929→5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
   930→6. `create_webhook_deliveries_table` - FK: `subscription_id`
   931→
   932→#### Step 10.2: Webhook Service
   933→
   934→**File to create:** `App\Services\WebhookService`
   935→
   936→| Method | Description |
   937→|--------|-------------|
   938→| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
   939→| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
   940→| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |
   941→
   942→**File to create:** `App\Jobs\DeliverWebhook`
   943→
   944→Requirements:
   945→- HTTP POST to target URL with JSON payload
   946→- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
   947→- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
   948→- Records response in `webhook_deliveries`
   949→- Circuit breaker: after 5 consecutive failures, pause subscription
   950→
   951→---
   952→
   953→### Phase 11: Polish
   954→
   955→**Priority:** LOW but important for completeness.
   956→
   957→1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
   958→2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
   959→3. Dark mode: `dark:` prefix on all storefront views and admin views
   960→4. Error pages: styled 404 and 503 pages matching the storefront theme
   961→5. Structured logging: JSON channel in `config/logging.php`
   962→6. Seed data: comprehensive seeders for demo stores
   963→
   964→---
   965→
   966→### Phase 12: Full Test Suite Execution
   967→
   968→**Priority:** Runs after all phases are complete.
   969→
   970→**Verification steps:**
   971→
   972→1. Run the full unit and feature test suite and confirm all tests pass
   973→2. Run all browser tests and confirm they pass
   974→3. Run the code style formatter/checker and confirm conformance
   975→4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
   976→5. Manual smoke: visit storefront, navigate products, add to cart, checkout
   977→6. Manual smoke: visit admin login, authenticate, manage products and orders
   978→
   979→---
   980→
   981→## 3. Pest Unit/Feature Test Specification
   982→
   983→All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.
   984→
   985→### Test Directory Structure
   986→
   987→| Directory | Files |
   988→|-----------|-------|
   989→| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
   990→| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
   991→| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
   992→| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
   993→| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
   994→| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
   995→| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
   996→| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
   997→| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
   998→| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
   999→| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |

type: text

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

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

#### Step 5.5: Refund Service

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

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

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

#### Step 5.6: Fulfillment Service

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

| Method | Description |
|--------|-------------|
| `create(Order $order, array $lines, ?array $tracking): Fulfillment` | Creates fulfillment with specified lines and quantities. **Fulfillment guard:** Throws exception if `financial_status` is not `paid` or `partially_refunded`. |
| `markAsShipped(Fulfillment $fulfillment, ?array $tracking): void` | Sets tracking info, transitions to `shipped`, sets `shipped_at` |
| `markAsDelivered(Fulfillment $fulfillment): void` | Transitions to `delivered`, sets `delivered_at`. Dispatches `FulfillmentDelivered` event. |

Updates `order.fulfillment_status` based on whether all lines are fulfilled (`fulfilled`) or some (`partial`).

**Fulfillment guard:** Before creating a fulfillment, the service checks that the order's `financial_status` is `paid` or `partially_refunded`. If the guard fails, a `FulfillmentGuardException` is thrown. This prevents shipping items before payment is confirmed (especially important for bank transfer orders).

**Events to create:**

| Type | Name | Purpose |
|------|------|---------|
| Event | `OrderCreated` | Dispatched when order is created from checkout |
| Event | `OrderPaid` | Dispatched when payment is captured |
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |
|-----------|-------|-------------|
| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |

Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.

---

### Phase 7: Admin Panel

**Priority:** MEDIUM - merchant management interface.

#### Step 7.1: Admin Layout

**File to create:** `resources/views/livewire/admin/layout/app.blade.php`

**Components to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
| `Admin\Layout\Breadcrumbs` | Dynamic breadcrumb trail |

Toast notifications via Livewire events.

#### Step 7.2: Dashboard

**File to create:** `Admin\Dashboard` Livewire component

Requirements:
- KPI tiles: total sales (period), order count, average order value, conversion rate
- Orders chart (last 30 days line chart)
- Recent orders table (last 10)
- Date range filtering via Livewire properties

#### Step 7.3: Product Management

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Products\Index` | Product list with search, status filter, bulk actions (archive, delete), pagination |
| `Admin\Products\Form` | Shared product form component used by both create and edit routes (create vs edit mode). Title, description (rich text), status, vendor, type, tags, variants builder, media upload. |

> **Shared-form pattern:** Create and Edit routes render the same Livewire form component in create vs edit mode. Shared form components follow the pattern `App\Livewire\Admin\{Resource}\Form`, used for both create and edit routes as defined in Spec 03. This applies to Products, Collections, Discounts, and Pages.

Variants builder: dynamic form to add options (Size, Color, etc.) and values, auto-generates variant matrix. Each variant row has price, SKU, barcode, weight, inventory quantity fields.

Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.

#### Step 7.4: Order Management

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Orders\Index` | Order list with status filters, search by order number/email, date range |
| `Admin\Orders\Show` | Order detail: timeline, line items, payment info, fulfillment modal, refund modal |

Fulfillment modal: select lines and quantities, add tracking info, submit.
Refund modal: enter amount (partial or full), reason, restock checkbox, submit.

#### Step 7.5: Other Admin Sections

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Collections\Index` | Collection list |
| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
| `Admin\Customers\Index` | Customer list with search |
| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
| `Admin\Discounts\Index` | Discount list with status/type filters |
| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
| `Admin\Pages\Index` | Content pages list |
| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
| `Admin\Apps\Index` | App directory |
| `Admin\Apps\Show` | Installed app detail |
| `Admin\Developers\Index` | API token management, webhook subscriptions |

---

### Phase 8: Search

**Priority:** LOW - enhances product discovery.

#### Step 8.1: Migrations

1. `create_search_settings_table` - PK: `store_id`
2. `create_search_queries_table` - FK: `store_id`
3. FTS5 virtual table migration (raw SQL) - create `products_fts` virtual table using FTS5 with columns: title, description, vendor, product_type, tags; content synced from `products` table

#### Step 8.2: Search Service

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

| Method | Description |
|--------|-------------|
| `search(Store $store, string $query, array $filters, int $perPage): LengthAwarePaginator` | FTS5 query with store scoping |
| `autocomplete(Store $store, string $prefix, int $limit): Collection` | Prefix matching for search-as-you-type |
| `syncProduct(Product $product): void` | Upsert into FTS5 index |
| `removeProduct(int $productId): void` | Delete from FTS5 index |

**File to create:** `App\Observers\ProductObserver` - calls `SearchService::syncProduct()` on create/update, `removeProduct()` on delete.

#### Step 8.3: Search UI

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Storefront\Search\Modal` | Search-as-you-type modal with autocomplete results |
| `Storefront\Search\Index` | Full search results page with filters (vendor, price range, collection), sort (relevance, price, newest), pagination |

---

### Phase 9: Analytics

**Priority:** LOW - reporting and insights.

#### Step 9.1: Migrations

1. `create_analytics_events_table` - FK: `store_id`, `customer_id`
2. `create_analytics_daily_table` - composite PK: `store_id` + `date`

#### Step 9.2: Analytics Service

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

| Method | Description |
|--------|-------------|
| `track(Store $store, string $type, array $properties, ?string $sessionId, ?int $customerId): void` | Inserts raw event |
| `getDailyMetrics(Store $store, string $startDate, string $endDate): Collection` | Reads pre-aggregated daily data |

Event types: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`.

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

Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

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

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |

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

Requirements:
- HTTP POST to target URL with JSON payload
- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
- Records response in `webhook_deliveries`
- Circuit breaker: after 5 consecutive failures, pause subscription

---

### Phase 11: Polish

**Priority:** LOW but important for completeness.

1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
3. Dark mode: `dark:` prefix on all storefront views and admin views
4. Error pages: styled 404 and 503 pages matching the storefront theme
5. Structured logging: JSON channel in `config/logging.php`
6. Seed data: comprehensive seeders for demo stores

---

### Phase 12: Full Test Suite Execution

**Priority:** Runs after all phases are complete.

**Verification steps:**

1. Run the full unit and feature test suite and confirm all tests pass
2. Run all browser tests and confirm they pass
3. Run the code style formatter/checker and confirm conformance
4. Run a fresh migration with seeding and confirm all migrations and seeders succeed without errors
5. Manual smoke: visit storefront, navigate products, add to cart, checkout
6. Manual smoke: visit admin login, authenticate, manage products and orders

---

## 3. Pest Unit/Feature Test Specification

All tests use Pest syntax. Feature tests use `RefreshDatabase` trait. Unit tests do not hit the database unless factories are involved.

### Test Directory Structure

| Directory | Files |
|-----------|-------|
| `tests/Unit/` | `PricingEngineTest.php`, `DiscountCalculatorTest.php`, `TaxCalculatorTest.php`, `ShippingCalculatorTest.php`, `CartVersionTest.php`, `HandleGeneratorTest.php` |
| `tests/Feature/Tenancy/` | `TenantResolutionTest.php`, `StoreIsolationTest.php` |
| `tests/Feature/Auth/` | `AdminAuthTest.php`, `CustomerAuthTest.php`, `SanctumTokenTest.php` |
| `tests/Feature/Products/` | `ProductCrudTest.php`, `VariantTest.php`, `InventoryTest.php`, `CollectionTest.php`, `MediaUploadTest.php` |
| `tests/Feature/Cart/` | `CartServiceTest.php`, `CartApiTest.php` |
| `tests/Feature/Checkout/` | `CheckoutFlowTest.php`, `CheckoutStateTest.php`, `PricingIntegrationTest.php`, `DiscountTest.php`, `ShippingTest.php`, `TaxTest.php` |
| `tests/Feature/Orders/` | `OrderCreationTest.php`, `RefundTest.php`, `FulfillmentTest.php` |
| `tests/Feature/Payments/` | `MockPaymentProviderTest.php`, `PaymentServiceTest.php`, `BankTransferConfirmationTest.php` |
| `tests/Feature/Customers/` | `CustomerAccountTest.php`, `AddressManagementTest.php` |
| `tests/Feature/Admin/` | `DashboardTest.php`, `ProductManagementTest.php`, `OrderManagementTest.php`, `DiscountManagementTest.php`, `SettingsTest.php` |
| `tests/Feature/Search/` | `SearchTest.php`, `AutocompleteTest.php` |