The user is asking whether the OMS (Order Management System) workflow differs per payment method and shipment method. Let me think about what the current spec says and what the real-world expectations are.

In real e-commerce:

**Per payment method differences:**
- **Credit Card:** Auth at checkout, auto-capture -> order is "paid" immediately. Fulfillment can start right away.
- **PayPal:** Similar to credit card - payment confirmed instantly. Order is "paid."
- **Bank Transfer:** Order is created but payment is NOT confirmed yet. Order sits in "pending" / "awaiting payment" state. Admin needs to manually confirm payment receipt (or an automated bank reconciliation does it). Only then can fulfillment begin.


Good question. Looking at what the spec currently defines - **no, it does not differentiate OMS workflow by payment method.** The current flow assumes every payment confirms instantly (the Stripe redirect model). That breaks with Bank Transfer.

### The Problem

The current `completeCheckout` procedure does this in one step:

```
Payment confirmed -> Create order (status=paid) -> Commit inventory -> Done
```

That works for **Card** and **PayPal** (instant confirmation), but **Bank Transfer** needs:

```
Order created (status=pending, financial=pending)
  -> Customer transfers money offline
  -> Admin manually confirms payment receipt
  -> Order becomes "paid"
  -> Fulfillment can begin
```

### What differs per payment method today vs. what should differ

| Concern | Card / PayPal (instant) | Bank Transfer (deferred) |
|---------|------------------------|--------------------------|
| Order created when? | On payment confirmation | On checkout completion (before payment) |
| Initial financial_status | `paid` | `pending` |
| Inventory reserved or committed? | Committed immediately | **Reserved** until payment confirmed, with a timeout |
| Admin action needed? | None | Must confirm payment receipt |
| Fulfillment allowed? | Immediately | Only after payment confirmed |
| Auto-cancel on non-payment? | N/A | After X days (configurable) |

### What differs per shipment method

The spec already handles digital vs. physical (Gift Card skips shipping). But it doesn't distinguish further:

| Concern | Physical (Standard/Express) | Digital (no shipping) |
|---------|----------------------------|----------------------|
| Fulfillment flow | Pick, pack, ship, enter tracking | Auto-fulfill on payment (email delivery) |
| Shipping step in checkout | Yes - zone matching, rate selection | Skipped entirely |
| Fulfillment status after payment | `unfulfilled` | Could auto-set to `fulfilled` |

**Currently missing:** digital products are not auto-fulfilled in the spec. The seeded Gift Card order (#1014) is manually marked as fulfilled. In a real shop, digital delivery would trigger automatically.

### Summary

The spec currently treats all payments as instant and all fulfillments as manual. To properly support the 3 payment methods, we need:

1. **A "pending payment" order state** for bank transfer (order exists, not yet paid, inventory reserved with timeout)
2. **Admin "confirm payment" action** on order detail page
3. **Auto-cancel job** for unpaid bank transfer orders after N days
4. **Auto-fulfillment** for digital-only orders on payment confirmation

Do you want me to account for these in the implementation plan, or do you want to keep it simpler (e.g., drop bank transfer, or treat it as "always instant" like the others)?


Based on our discussion, adjust the specs. OMS needs to be in place (not the simple solution)


Events (like "shipped") are triggerd by admin buttnos


The user wants me to adjust the specs based on our discussion. Let me summarize the changes needed:

1. **Onsite checkout with mock PSP** - Replace Stripe redirect / PayPal stub with:
   - 3 payment methods: Credit Card, PayPal, Bank Transfer (all mocked)
   - Magic card numbers for test failures
   - Auto-capture on checkout for Card/PayPal
   - Deferred payment flow for Bank Transfer

2. **OMS workflow differentiation per payment method:**
   - Card/PayPal: instant auth+capture, order created as "paid", fulfillment allowed immediately
   - Bank Transfer: order created as "pending", inventory reserved with timeout, admin confirms payment, then fulfillment allowed
   - Auto-cancel job for unpaid bank transfer orders

3. **OMS workflow differentiation per shipment method:**
   - Physical: manual fulfillment (pick, pack, ship, tracking)
   - Digital: auto-fulfillment on payment confirmation

Now I'm mapping out which spec files need changes and planning the scope. The main files are 05-BUSINESS-LOGIC.md (payment processing and OMS workflows), 02-API-ROUTES.md (checkout and admin payment confirmation endpoints), 03-ADMIN-UI.md (order detail page with confirm payment button), 04-STOREFRONT-UI.md (payment method selection and onsite forms), plus updates to database schema, seeders, E2E tests, and the implementation roadmap.

Before making edits, I need to think through the design decisions: payment method enum values, database columns for tracking payment method on checkouts and orders, how payment statuses flow differently per method, order financial status transitions, inventory reservation mechanics with timeout, and the auto-cancel job logic. I also need to clarify that fulfillment is blocked while financial_status is pending, and digital orders auto-fulfill once paid.

I should read the full relevant sections of the files I'm modifying to understand the current structure before making changes, particularly the payment processing section in 05-BUSINESS-LOGIC.md and the order/fulfillment management sections that follow it.


I'll need to read the full sections that need updating across multiple spec files. Let me do that in parallel.


   997→---
   998→
   999→## 11. Order Management
  1000→
  1001→### 11.1 Order Creation (from Completed Checkout)
  1002→
  1003→See Section 6.2 "Transition: payment_pending -> completed" for the full procedure. The order creation is the central action within the completeCheckout transaction.
  1004→
  1005→**Key requirements for the order record:**
  1006→- Snapshot all checkout data (addresses, email, totals) at time of order creation.
  1007→- Order lines must contain snapshot data (`title_snapshot`, `sku_snapshot`) so that order history survives product changes.
  1008→- The raw payment provider response must be encrypted before storage (PCI compliance).
  1009→- The entire operation must be atomic (single database transaction).
  1010→
  1011→### 11.2 Order Numbering
  1012→
  1013→- Sequential per store, starting at 1001.
  1014→- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
  1015→- Generation: within the order creation transaction, find the maximum existing order number for the store and increment by 1.
  1016→- Since this runs inside a SQLite transaction, there is no risk of duplicate numbers.
  1017→
  1018→### 11.3 Order Statuses
  1019→
  1020→Three independent status dimensions tracked on each order:
  1021→
  1022→**Order status** (`orders.status`):
  1023→
  1024→| Status | Description |
  1025→|---|---|
  1026→| `pending` | Order created, awaiting processing |
  1027→| `paid` | Payment confirmed |
  1028→| `fulfilled` | All items shipped |
  1029→| `cancelled` | Order cancelled before fulfillment |
  1030→| `refunded` | Fully refunded |
  1031→
  1032→**Financial status** (`orders.financial_status`):
  1033→
  1034→| Status | Description |
  1035→|---|---|
  1036→| `pending` | Payment not yet confirmed |
  1037→| `authorized` | Payment authorized but not captured |
  1038→| `paid` | Payment captured successfully |
  1039→| `partially_refunded` | Some items or partial amount refunded |
  1040→| `refunded` | Fully refunded |
  1041→| `voided` | Authorization voided (no capture occurred) |
  1042→
  1043→**Fulfillment status** (`orders.fulfillment_status`):
  1044→
  1045→| Status | Description |
  1046→|---|---|
  1047→| `unfulfilled` | No items have been shipped |
  1048→| `partial` | Some items shipped, others pending |
  1049→| `fulfilled` | All items shipped |
  1050→
  1051→### 11.4 Refund Processing
  1052→
  1053→**Service:** `App\Services\RefundService`
  1054→
  1055→**Input (RefundRequest):**
  1056→
  1057→| Field | Type | Required | Description |
  1058→|---|---|---|---|
  1059→| `amount` | integer (cents) | No | Custom refund amount. If not set, calculated from lines. |
  1060→| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
  1061→| `reason` | string | No | Text reason for the refund. |
  1062→| `restock` | boolean | No | Whether to restock inventory for refunded items. |
  1063→
  1064→**Process:**
  1065→
  1066→```
  1067→PROCEDURE processRefund(order, request):
  1068→    1. Calculate remaining refundable amount:
  1069→       refundable = order.total_amount - SUM(existing refunds for this order)
  1070→
  1071→    2. IF request.amount is set:
  1072→           VALIDATE request.amount <= refundable
  1073→       ELSE IF request.lines is set:
  1074→           CALCULATE amount from specified lines and quantities
  1075→       ELSE:
  1076→           amount = refundable (full refund)
  1077→
  1078→    3. CALL PaymentProvider.refund(order's payment, amount)
  1079→
  1080→    4. CREATE Refund record with status = 'pending'
  1081→
  1082→    5. On provider confirmation:
  1083→       UPDATE refund status = 'processed'
  1084→
  1085→    6. Calculate total refunded for this order:
  1086→       IF total refunded = order.total_amount:
  1087→           SET order.financial_status = 'refunded'
  1088→           SET order.status = 'refunded'
  1089→       ELSE:
  1090→           SET order.financial_status = 'partially_refunded'
  1091→
  1092→    7. IF request.restock = true:
  1093→       FOR EACH refunded line and quantity:
  1094→           CALL InventoryService.restock(variant, quantity)
  1095→
  1096→    8. DISPATCH OrderRefunded event
  1097→```
  1098→
  1099→### 11.5 Fulfillment
  1100→
  1101→**Service:** `App\Services\FulfillmentService`
  1102→
  1103→**Creating a fulfillment:**
  1104→
  1105→Input:
  1106→- `lines` - map of order_line_id to quantity, specifying which items are in this shipment
  1107→- `trackingData` - tracking_company, tracking_number, tracking_url
  1108→
  1109→**Process:**
  1110→
  1111→```
  1112→PROCEDURE createFulfillment(order, lines, trackingData):
  1113→    1. FOR EACH line in request:
  1114→       fulfilledSoFar = SUM(fulfillment_lines quantities for this order_line)
  1115→       unfulfilled = order_line.quantity - fulfilledSoFar
  1116→       VALIDATE requested quantity <= unfulfilled
  1117→
  1118→    2. CREATE Fulfillment record with status = 'pending' and tracking data
  1119→
  1120→    3. CREATE fulfillment_lines for each order line and quantity
  1121→
  1122→    4. Determine new fulfillment status:
  1123→       FOR EACH order_line in order:
  1124→           totalFulfilled = SUM(all fulfillment_lines for this order_line)
  1125→           IF totalFulfilled < order_line.quantity:
  1126→               allFulfilled = false
  1127→
  1128→       IF allFulfilled:
  1129→           SET order.fulfillment_status = 'fulfilled'
  1130→           SET order.status = 'fulfilled'
  1131→       ELSE:
  1132→           SET order.fulfillment_status = 'partial'
  1133→
  1134→    5. DISPATCH FulfillmentCreated event
  1135→```
  1136→
  1137→**Fulfillment status transitions:**
  1138→
  1139→```mermaid
  1140→stateDiagram-v2
  1141→    [*] --> pending : Fulfillment created
  1142→    pending --> shipped : Merchant marks as shipped
  1143→    shipped --> delivered : Merchant marks as delivered
  1144→
  1145→    note right of shipped : Sets shipped_at timestamp, sends tracking email
  1146→```
  1147→
  1148→When transitioning to `shipped`:
  1149→- Set `shipped_at` timestamp.
  1150→- Dispatch `FulfillmentShipped` event.
  1151→- Send email notification to customer with tracking information.
  1152→
  1153→### 11.6 Order Export
  1154→
  1155→`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.
  1156→
  1157→**CSV columns:** `order_number`, `created_at`, `status`, `financial_status`, `fulfillment_status`, `customer_email`, `customer_name`, `subtotal_amount`, `discount_amount`, `shipping_amount`, `tax_amount`, `total_amount`, `currency`, `shipping_method`, `tracking_number`
  1158→
  1159→All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.
  1160→
  1161→---
  1162→
  1163→## 12. Customer Accounts
  1164→
  1165→### 12.1 Registration
  1166→
  1167→**Service:** `App\Services\CustomerService`
  1168→
  1169→**Required fields:**
  1170→
  1171→| Field | Validation |
  1172→|---|---|
  1173→| `name` | Required, string |
  1174→| `email` | Required, valid email, unique per store |
  1175→| `password` | Required, minimum 8 characters |
  1176→| `marketing_opt_in` | Optional, boolean, default false |
  1177→
  1178→- Email uniqueness enforced by the database unique index on `(store_id, email)`.
  1179→- Password hashed using Laravel's Hash facade (bcrypt by default).
  1180→
  1181→### 12.2 Authentication
  1182→
  1183→**Separate auth guard:** `customer`
  1184→
  1185→Requirements:
  1186→- Configure a dedicated `customer` guard in auth config, using session driver.
  1187→- Configure a dedicated `customers` provider using the Customer model.
  1188→- Login: email + password, scoped to the current store.
  1189→- Password reset: standard Laravel password reset flow adapted for the customer guard and broker.
  1190→
  1191→**Store isolation:** Customer accounts are per-store. The same email address can exist in different stores as separate accounts. Authentication always scopes the query to the current store.
  1192→
  1193→### 12.3 Data Access
  1194→
  1195→- Customers can only view their own orders (scoped by customer_id match).
  1196→- Customers can only manage their own addresses (scoped by customer_id match).
  1197→- All customer queries include the StoreScope global scope for tenant isolation.
  1198→
  1199→### 12.4 Guest Cart to Customer Linking
  1200→
  1201→```
  1202→PROCEDURE linkGuestToCustomer(checkoutEmail, storeId, order):
  1203→    existingCustomer = find customer by email in store
  1204→    IF existingCustomer:
  1205→        LINK order to existingCustomer
  1206→    ELSE:
  1207→        CREATE new customer with:
  1208→            - email = checkoutEmail
  1209→            - password_hash = null (guest customer)
  1210→            - store_id = storeId
  1211→        LINK order to new customer
  1212→
  1213→    // Guest customer can later "claim" their account
  1214→    // by setting a password via registration or account-claim flow
  1215→```
  1216→
  1217→---
  1218→
  1219→## 13. Webhook Delivery System
  1220→
  1221→### 13.1 Subscription Model
  1222→
  1223→Stores (or app installations) subscribe to event types via `webhook_subscriptions`.
  1224→
  1225→**Supported event types:**
  1226→
  1227→| Event Type | Triggered By |
  1228→|---|---|
  1229→| `order.created` | OrderCreated event |
  1230→| `order.paid` | Payment captured successfully |
  1231→| `order.fulfilled` | All order lines fulfilled |
  1232→| `order.refunded` | Refund processed |
  1233→| `product.created` | New product saved |
  1234→| `product.updated` | Product modified |
  1235→| `product.deleted` | Product deleted or archived |
  1236→| `checkout.completed` | Checkout successfully completed |
  1237→
  1238→### 13.2 Delivery Process
  1239→
  1240→**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.
  1241→
  1242→**Job:** `App\Jobs\DeliverWebhook`
  1243→
  1244→```mermaid
  1245→sequenceDiagram
  1246→    participant DomainEvent
  1247→    participant Listener
  1248→    participant Queue
  1249→    participant Job
  1250→    participant TargetURL
  1251→
  1252→    DomainEvent->>Listener: Event fired (e.g., OrderCreated)
  1253→    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
  1254→    loop For each matching subscription
  1255→        Listener->>Queue: Dispatch DeliverWebhook job
  1256→    end
  1257→    Queue->>Job: Process job
  1258→    Job->>Job: Serialize event payload as JSON
  1259→    Job->>Job: Generate HMAC-SHA256 signature using subscription's signing_secret (decrypted)
  1260→    Job->>TargetURL: HTTP POST with headers and payload
  1261→    TargetURL-->>Job: Response
  1262→    Job->>Job: Record result in webhook_deliveries
  1263→```
  1264→
  1265→**Required HTTP headers on delivery:**
  1266→
  1267→| Header | Value |
  1268→|---|---|
  1269→| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
  1270→| `X-Platform-Event` | Event type string (e.g., `order.created`) |
  1271→| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
  1272→| `X-Platform-Timestamp` | Unix timestamp of the event |
  1273→| `Content-Type` | `application/json` |
  1274→
  1275→### 13.3 Retry Logic
  1276→
  1277→On non-2xx response or connection timeout:
  1278→
  1279→| Attempt | Backoff Delay |
  1280→|---|---|
  1281→| 1 (initial) | Immediate |
  1282→| 2 | 1 minute |
  1283→| 3 | 5 minutes |
  1284→| 4 | 30 minutes |
  1285→| 5 | 2 hours |
  1286→| 6 (final) | 12 hours |
  1287→
  1288→After 5 retry attempts (6 total), mark the delivery as `failed` (dead letter).
  1289→
  1290→Use Laravel's built-in job retry mechanism with the backoff delays configured as: 60, 300, 1800, 7200, 43200 seconds.
  1291→
  1292→### 13.4 Circuit Breaker
  1293→
  1294→- Track consecutive failures per subscription (across different events).
  1295→- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
  1296→- Log a warning and (future) send an admin notification.

---

## 11. Order Management

### 11.1 Order Creation (from Completed Checkout)

See Section 6.2 "Transition: payment_pending -> completed" for the full procedure. The order creation is the central action within the completeCheckout transaction.

**Key requirements for the order record:**
- Snapshot all checkout data (addresses, email, totals) at time of order creation.
- Order lines must contain snapshot data (`title_snapshot`, `sku_snapshot`) so that order history survives product changes.
- The raw payment provider response must be encrypted before storage (PCI compliance).
- The entire operation must be atomic (single database transaction).

### 11.2 Order Numbering

- Sequential per store, starting at 1001.
- Format: configurable prefix (default: `#`) stored in `store_settings.settings_json` key `order_number_prefix`.
- Generation: within the order creation transaction, find the maximum existing order number for the store and increment by 1.
- Since this runs inside a SQLite transaction, there is no risk of duplicate numbers.

### 11.3 Order Statuses

Three independent status dimensions tracked on each order:

**Order status** (`orders.status`):

| Status | Description |
|---|---|
| `pending` | Order created, awaiting processing |
| `paid` | Payment confirmed |
| `fulfilled` | All items shipped |
| `cancelled` | Order cancelled before fulfillment |
| `refunded` | Fully refunded |

**Financial status** (`orders.financial_status`):

| Status | Description |
|---|---|
| `pending` | Payment not yet confirmed |
| `authorized` | Payment authorized but not captured |
| `paid` | Payment captured successfully |
| `partially_refunded` | Some items or partial amount refunded |
| `refunded` | Fully refunded |
| `voided` | Authorization voided (no capture occurred) |

**Fulfillment status** (`orders.fulfillment_status`):

| Status | Description |
|---|---|
| `unfulfilled` | No items have been shipped |
| `partial` | Some items shipped, others pending |
| `fulfilled` | All items shipped |

### 11.4 Refund Processing

**Service:** `App\Services\RefundService`

**Input (RefundRequest):**

| Field | Type | Required | Description |
|---|---|---|---|
| `amount` | integer (cents) | No | Custom refund amount. If not set, calculated from lines. |
| `lines` | map of order_line_id to quantity | No | Which lines and quantities to refund (for line-level refunds). |
| `reason` | string | No | Text reason for the refund. |
| `restock` | boolean | No | Whether to restock inventory for refunded items. |

**Process:**

```
PROCEDURE processRefund(order, request):
    1. Calculate remaining refundable amount:
       refundable = order.total_amount - SUM(existing refunds for this order)

    2. IF request.amount is set:
           VALIDATE request.amount <= refundable
       ELSE IF request.lines is set:
           CALCULATE amount from specified lines and quantities
       ELSE:
           amount = refundable (full refund)

    3. CALL PaymentProvider.refund(order's payment, amount)

    4. CREATE Refund record with status = 'pending'

    5. On provider confirmation:
       UPDATE refund status = 'processed'

    6. Calculate total refunded for this order:
       IF total refunded = order.total_amount:
           SET order.financial_status = 'refunded'
           SET order.status = 'refunded'
       ELSE:
           SET order.financial_status = 'partially_refunded'

    7. IF request.restock = true:
       FOR EACH refunded line and quantity:
           CALL InventoryService.restock(variant, quantity)

    8. DISPATCH OrderRefunded event
```

### 11.5 Fulfillment

**Service:** `App\Services\FulfillmentService`

**Creating a fulfillment:**

Input:
- `lines` - map of order_line_id to quantity, specifying which items are in this shipment
- `trackingData` - tracking_company, tracking_number, tracking_url

**Process:**

```
PROCEDURE createFulfillment(order, lines, trackingData):
    1. FOR EACH line in request:
       fulfilledSoFar = SUM(fulfillment_lines quantities for this order_line)
       unfulfilled = order_line.quantity - fulfilledSoFar
       VALIDATE requested quantity <= unfulfilled

    2. CREATE Fulfillment record with status = 'pending' and tracking data

    3. CREATE fulfillment_lines for each order line and quantity

    4. Determine new fulfillment status:
       FOR EACH order_line in order:
           totalFulfilled = SUM(all fulfillment_lines for this order_line)
           IF totalFulfilled < order_line.quantity:
               allFulfilled = false

       IF allFulfilled:
           SET order.fulfillment_status = 'fulfilled'
           SET order.status = 'fulfilled'
       ELSE:
           SET order.fulfillment_status = 'partial'

    5. DISPATCH FulfillmentCreated event
```

**Fulfillment status transitions:**

```mermaid
stateDiagram-v2
    [*] --> pending : Fulfillment created
    pending --> shipped : Merchant marks as shipped
    shipped --> delivered : Merchant marks as delivered

    note right of shipped : Sets shipped_at timestamp, sends tracking email
```

When transitioning to `shipped`:
- Set `shipped_at` timestamp.
- Dispatch `FulfillmentShipped` event.
- Send email notification to customer with tracking information.

### 11.6 Order Export

`GET /api/admin/v1/orders/export` returns a CSV file of orders matching the current filter criteria.

**CSV columns:** `order_number`, `created_at`, `status`, `financial_status`, `fulfillment_status`, `customer_email`, `customer_name`, `subtotal_amount`, `discount_amount`, `shipping_amount`, `tax_amount`, `total_amount`, `currency`, `shipping_method`, `tracking_number`

All monetary values are exported in minor units (cents). Dates use ISO-8601 format. The response uses `Content-Type: text/csv` with `Content-Disposition: attachment; filename="orders-{date}.csv"`.

---

## 12. Customer Accounts

### 12.1 Registration

**Service:** `App\Services\CustomerService`

**Required fields:**

| Field | Validation |
|---|---|
| `name` | Required, string |
| `email` | Required, valid email, unique per store |
| `password` | Required, minimum 8 characters |
| `marketing_opt_in` | Optional, boolean, default false |

- Email uniqueness enforced by the database unique index on `(store_id, email)`.
- Password hashed using Laravel's Hash facade (bcrypt by default).

### 12.2 Authentication

**Separate auth guard:** `customer`

Requirements:
- Configure a dedicated `customer` guard in auth config, using session driver.
- Configure a dedicated `customers` provider using the Customer model.
- Login: email + password, scoped to the current store.
- Password reset: standard Laravel password reset flow adapted for the customer guard and broker.

**Store isolation:** Customer accounts are per-store. The same email address can exist in different stores as separate accounts. Authentication always scopes the query to the current store.

### 12.3 Data Access

- Customers can only view their own orders (scoped by customer_id match).
- Customers can only manage their own addresses (scoped by customer_id match).
- All customer queries include the StoreScope global scope for tenant isolation.

### 12.4 Guest Cart to Customer Linking

```
PROCEDURE linkGuestToCustomer(checkoutEmail, storeId, order):
    existingCustomer = find customer by email in store
    IF existingCustomer:
        LINK order to existingCustomer
    ELSE:
        CREATE new customer with:
            - email = checkoutEmail
            - password_hash = null (guest customer)
            - store_id = storeId
        LINK order to new customer

    // Guest customer can later "claim" their account
    // by setting a password via registration or account-claim flow
```

---

## 13. Webhook Delivery System

### 13.1 Subscription Model

Stores (or app installations) subscribe to event types via `webhook_subscriptions`.

**Supported event types:**

| Event Type | Triggered By |
|---|---|
| `order.created` | OrderCreated event |
| `order.paid` | Payment captured successfully |
| `order.fulfilled` | All order lines fulfilled |
| `order.refunded` | Refund processed |
| `product.created` | New product saved |
| `product.updated` | Product modified |
| `product.deleted` | Product deleted or archived |
| `checkout.completed` | Checkout successfully completed |

### 13.2 Delivery Process

**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.

**Job:** `App\Jobs\DeliverWebhook`

```mermaid
sequenceDiagram
    participant DomainEvent
    participant Listener
    participant Queue
    participant Job
    participant TargetURL

    DomainEvent->>Listener: Event fired (e.g., OrderCreated)
    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
    loop For each matching subscription
        Listener->>Queue: Dispatch DeliverWebhook job
    end
    Queue->>Job: Process job
    Job->>Job: Serialize event payload as JSON
    Job->>Job: Generate HMAC-SHA256 signature using subscription's signing_secret (decrypted)
    Job->>TargetURL: HTTP POST with headers and payload
    TargetURL-->>Job: Response
    Job->>Job: Record result in webhook_deliveries
```

**Required HTTP headers on delivery:**

| Header | Value |
|---|---|
| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
| `X-Platform-Event` | Event type string (e.g., `order.created`) |
| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
| `X-Platform-Timestamp` | Unix timestamp of the event |
| `Content-Type` | `application/json` |

### 13.3 Retry Logic

On non-2xx response or connection timeout:

| Attempt | Backoff Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 (final) | 12 hours |

After 5 retry attempts (6 total), mark the delivery as `failed` (dead letter).

Use Laravel's built-in job retry mechanism with the backoff delays configured as: 60, 300, 1800, 7200, 43200 seconds.

### 13.4 Circuit Breaker

- Track consecutive failures per subscription (across different events).
- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
- Log a warning and (future) send an admin notification.


   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,
   301→    "item_count": 2
   302→  },
   303→  "created_at": "2026-02-11T10:00:00Z",
   304→  "updated_at": "2026-02-11T10:05:00Z"
   305→}
   306→```
   307→
   308→**Error responses:**
   309→- `404` - Cart not found
   310→
   311→---
   312→
   313→#### POST /api/storefront/v1/carts/{cartId}/lines
   314→
   315→Add a line item to the cart.
   316→
   317→- **Authentication:** None
   318→- **Rate limit:** `api.storefront` (120/min per IP)
   319→
   320→**Request body:**
   321→
   322→```json
   323→{
   324→  "variant_id": 101,
   325→  "quantity": 2
   326→}
   327→```
   328→
   329→| Field | Type | Required | Validation |
   330→|-------|------|----------|------------|
   331→| `variant_id` | integer | Yes | Must exist, belong to an active product in the current store |
   332→| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |
   333→
   334→**Success response:** `201 Created`
   335→
   336→Returns the full cart object (same schema as GET /carts/{cartId}).
   337→
   338→**Error responses:**
   339→- `404` - Cart not found
   340→- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
   341→- `422` - Validation error
   342→
   343→```json
   344→{
   345→  "message": "The given data was invalid.",
   346→  "errors": {
   347→    "variant_id": ["The selected variant is out of stock."]
   348→  }
   349→}
   350→```
   351→
   352→---
   353→
   354→#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}
   355→
   356→Update the quantity of a cart line.
   357→
   358→- **Authentication:** None
   359→- **Rate limit:** `api.storefront` (120/min per IP)
   360→
   361→**Request body:**
   362→
   363→```json
   364→{
   365→  "quantity": 3,
   366→  "cart_version": 3
   367→}
   368→```
   369→
   370→| Field | Type | Required | Validation |
   371→|-------|------|----------|------------|
   372→| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |
   373→| `cart_version` | integer | Yes | Must match current cart version for optimistic concurrency. |
   374→
   375→**Success response:** `200 OK`
   376→
   377→Returns the full cart object with updated totals.
   378→
   379→**Error responses:**
   380→- `404` - Cart or line not found
   381→- `409` - Version conflict
   382→- `422` - Validation error (e.g., exceeds inventory)
   383→
   384→---
   385→
   386→#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}
   387→
   388→Remove a line item from the cart.
   389→
   390→- **Authentication:** None
   391→- **Rate limit:** `api.storefront` (120/min per IP)
   392→
   393→**Request body:**
   394→
   395→```json
   396→{
   397→  "cart_version": 3
   398→}
   399→```
   400→
   401→| Field | Type | Required | Validation |
   402→|-------|------|----------|------------|
   403→| `cart_version` | integer | Yes | Must match current cart version. |
   404→
   405→**Success response:** `200 OK`
   406→
   407→Returns the full cart object.
   408→
   409→**Error responses:**
   410→- `404` - Cart or line not found
   411→- `409` - Version conflict
   412→
   413→---
   414→
   415→### 2.2 Checkout Endpoints
   416→
   417→#### Checkout Flow Sequence Diagram
   418→
   419→```mermaid
   420→sequenceDiagram
   421→    participant C as Customer Browser
   422→    participant API as Storefront API
   423→    participant Stripe as Stripe API
   424→    participant WH as Webhook Handler
   425→
   426→    C->>API: POST /checkouts (cart_id, email)
   427→    API-->>C: 201 Checkout (status: started)
   428→
   429→    C->>API: PUT /checkouts/{id}/address
   430→    API-->>C: 200 Checkout (status: addressed, available_shipping_methods)
   431→
   432→    C->>API: PUT /checkouts/{id}/shipping-method
   433→    API-->>C: 200 Checkout (status: shipping_selected, totals with tax)
   434→
   435→    opt Apply discount
   436→        C->>API: POST /checkouts/{id}/apply-discount
   437→        API-->>C: 200 Checkout (totals recalculated)
   438→    end
   439→
   440→    alt Stripe Checkout Session
   441→        C->>API: POST /payments/stripe/checkout-session
   442→        API->>Stripe: Create Checkout Session
   443→        Stripe-->>API: session_url
   444→        API-->>C: 201 (session_url)
   445→        C->>Stripe: Redirect to hosted payment page
   446→        Stripe-->>C: Redirect to success_url
   447→        Stripe->>WH: POST /webhooks/stripe (payment_intent.succeeded)
   448→        WH-->>Stripe: 200 OK
   449→    else Stripe Elements (embedded)
   450→        C->>API: POST /payments/stripe/payment-intent
   451→        API->>Stripe: Create PaymentIntent
   452→        Stripe-->>API: client_secret
   453→        API-->>C: 201 (client_secret)
   454→        C->>Stripe: Confirm payment (client-side)
   455→        Stripe->>WH: POST /webhooks/stripe (payment_intent.succeeded)
   456→        WH-->>Stripe: 200 OK
   457→    else PayPal
   458→        C->>API: POST /payments/paypal/create-order
   459→        API-->>C: 201 (approval_url)
   460→        C->>C: Redirect to PayPal approval
   461→        Note over WH: PayPal sends PAYMENT.CAPTURE.COMPLETED
   462→        WH-->>C: 200 OK
   463→    end
   464→
   465→    C->>API: GET /checkouts/{id} (poll or redirect)
   466→    API-->>C: 200 Checkout (status: completed)
   467→```
   468→
   469→#### POST /api/storefront/v1/checkouts
   470→
   471→Create a checkout from a cart.
   472→
   473→- **Authentication:** None or session-based
   474→- **Rate limit:** `checkout` (10/min per session)
   475→
   476→**Request body:**
   477→
   478→```json
   479→{
   480→  "cart_id": 42,
   481→  "email": "customer@example.com"
   482→}
   483→```
   484→
   485→| Field | Type | Required | Validation |
   486→|-------|------|----------|------------|
   487→| `cart_id` | integer | Yes | Must reference an active cart with at least one line. |
   488→| `email` | string | Yes | Valid email address. |
   489→
   490→**Success response:** `201 Created`
   491→
   492→```json
   493→{
   494→  "id": 7,
   495→  "store_id": 1,
   496→  "cart_id": 42,
   497→  "customer_id": null,
   498→  "status": "started",
   499→  "email": "customer@example.com",

| 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,
    "item_count": 2
  },
  "created_at": "2026-02-11T10:00:00Z",
  "updated_at": "2026-02-11T10:05:00Z"
}
```

**Error responses:**
- `404` - Cart not found

---

#### POST /api/storefront/v1/carts/{cartId}/lines

Add a line item to the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "variant_id": 101,
  "quantity": 2
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `variant_id` | integer | Yes | Must exist, belong to an active product in the current store |
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |

**Success response:** `201 Created`

Returns the full cart object (same schema as GET /carts/{cartId}).

**Error responses:**
- `404` - Cart not found
- `409` - Cart version conflict (concurrent modification, retry with fresh cart_version)
- `422` - Validation error

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "variant_id": ["The selected variant is out of stock."]
  }
}
```

---

#### PUT /api/storefront/v1/carts/{cartId}/lines/{lineId}

Update the quantity of a cart line.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "quantity": 3,
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `quantity` | integer | Yes | Min: 1, max: 9999. Must not exceed available inventory when policy is `deny`. |
| `cart_version` | integer | Yes | Must match current cart version for optimistic concurrency. |

**Success response:** `200 OK`

Returns the full cart object with updated totals.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict
- `422` - Validation error (e.g., exceeds inventory)

---

#### DELETE /api/storefront/v1/carts/{cartId}/lines/{lineId}

Remove a line item from the cart.

- **Authentication:** None
- **Rate limit:** `api.storefront` (120/min per IP)

**Request body:**

```json
{
  "cart_version": 3
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_version` | integer | Yes | Must match current cart version. |

**Success response:** `200 OK`

Returns the full cart object.

**Error responses:**
- `404` - Cart or line not found
- `409` - Version conflict

---

### 2.2 Checkout Endpoints

#### Checkout Flow Sequence Diagram

```mermaid
sequenceDiagram
    participant C as Customer Browser
    participant API as Storefront API
    participant Stripe as Stripe API
    participant WH as Webhook Handler

    C->>API: POST /checkouts (cart_id, email)
    API-->>C: 201 Checkout (status: started)

    C->>API: PUT /checkouts/{id}/address
    API-->>C: 200 Checkout (status: addressed, available_shipping_methods)

    C->>API: PUT /checkouts/{id}/shipping-method
    API-->>C: 200 Checkout (status: shipping_selected, totals with tax)

    opt Apply discount
        C->>API: POST /checkouts/{id}/apply-discount
        API-->>C: 200 Checkout (totals recalculated)
    end

    alt Stripe Checkout Session
        C->>API: POST /payments/stripe/checkout-session
        API->>Stripe: Create Checkout Session
        Stripe-->>API: session_url
        API-->>C: 201 (session_url)
        C->>Stripe: Redirect to hosted payment page
        Stripe-->>C: Redirect to success_url
        Stripe->>WH: POST /webhooks/stripe (payment_intent.succeeded)
        WH-->>Stripe: 200 OK
    else Stripe Elements (embedded)
        C->>API: POST /payments/stripe/payment-intent
        API->>Stripe: Create PaymentIntent
        Stripe-->>API: client_secret
        API-->>C: 201 (client_secret)
        C->>Stripe: Confirm payment (client-side)
        Stripe->>WH: POST /webhooks/stripe (payment_intent.succeeded)
        WH-->>Stripe: 200 OK
    else PayPal
        C->>API: POST /payments/paypal/create-order
        API-->>C: 201 (approval_url)
        C->>C: Redirect to PayPal approval
        Note over WH: PayPal sends PAYMENT.CAPTURE.COMPLETED
        WH-->>C: 200 OK
    end

    C->>API: GET /checkouts/{id} (poll or redirect)
    API-->>C: 200 Checkout (status: completed)
```

#### POST /api/storefront/v1/checkouts

Create a checkout from a cart.

- **Authentication:** None or session-based
- **Rate limit:** `checkout` (10/min per session)

**Request body:**

```json
{
  "cart_id": 42,
  "email": "customer@example.com"
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `cart_id` | integer | Yes | Must reference an active cart with at least one line. |
| `email` | string | Yes | Valid email address. |

**Success response:** `201 Created`

```json
{
  "id": 7,
  "store_id": 1,
  "cart_id": 42,
  "customer_id": null,
  "status": "started",
  "email": "customer@example.com",


   100→
   101→Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.
   102→
   103→#### Flux Component Mapping
   104→
   105→| UI Element | Flux Component |
   106→|-----------|----------------|
   107→| Platform logo and name | `brand` |
   108→| Navigation icons | `icon` |
   109→| Group dividers | `separator` |
   110→
   111→#### Responsive Behavior
   112→
   113→- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
   114→- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.
   115→
   116→### 1.3 Top Bar
   117→
   118→**Livewire component:** `App\Livewire\Admin\Layout\TopBar`
   119→
   120→**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`
   121→
   122→#### Component Requirements
   123→
   124→| Property | Type | Purpose |
   125→|----------|------|---------|
   126→| `currentStoreName` | `string` | Name of the active store |
   127→| `stores` | `Collection` | List of stores the user has access to |
   128→| `unreadNotificationCount` | `int` | Count of unread notifications |
   129→
   130→| Action | Behavior |
   131→|--------|----------|
   132→| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |
   133→
   134→#### Wireframe
   135→
   136→```
   137→+--------------------------------------------------------------------+
   138→| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
   139→+--------------------------------------------------------------------+
   140→  * hamburger visible on mobile only
   141→
   142→  Store Name dropdown:
   143→    - Store A
   144→    - Store B
   145→    - Store C
   146→
   147→  Avatar dropdown:
   148→    - Settings (links to /admin/settings)
   149→    - [separator]
   150→    - Log out (POST /logout with CSRF)
   151→```
   152→
   153→#### Interaction Flows
   154→
   155→- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
   156→- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
   157→- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.
   158→
   159→#### Flux Component Mapping
   160→
   161→| UI Element | Flux Component |
   162→|-----------|----------------|
   163→| Store selector | `dropdown` |
   164→| Hamburger (mobile) | `button` (icon: bars-3) |
   165→| Notification bell | `button` (icon: bell, variant: ghost) |
   166→| Notification count | `badge` |
   167→| User profile trigger | `profile` |
   168→| Profile menu | `dropdown` |
   169→| Menu divider | `separator` |
   170→
   171→### 1.4 Main Content Area
   172→
   173→The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.
   174→
   175→**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".
   176→
   177→**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.
   178→
   179→### 1.5 Toast Notifications
   180→
   181→A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.
   182→
   183→| Property | Value |
   184→|----------|-------|
   185→| Event name | `toast` |
   186→| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
   187→| Position | Top-right corner |
   188→| Auto-dismiss | 5 seconds |
   189→| Stacking | Supports multiple toasts |
   190→| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |
   191→
   192→Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").
   193→
   194→### 1.6 Dark Mode
   195→
   196→The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.
   197→
   198→Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.
   199→
   200→---
   201→
   202→## 2. Dashboard
   203→
   204→**Route:** `GET /admin`
   205→
   206→**Livewire component:** `App\Livewire\Admin\Dashboard`
   207→
   208→**Blade view:** `resources/views/livewire/admin/dashboard.blade.php`
   209→
   210→### Component Requirements
   211→
   212→| Property | Type | Purpose |
   213→|----------|------|---------|
   214→| `dateRange` | `string` | Active date range preset: `today`, `last_7_days`, `last_30_days`, `custom` |
   215→| `customStartDate` | `?string` | Custom range start (Y-m-d) |
   216→| `customEndDate` | `?string` | Custom range end (Y-m-d) |
   217→| `totalSales` | `int` | Total sales amount in minor units |
   218→| `ordersCount` | `int` | Number of orders in range |
   219→| `averageOrderValue` | `int` | Average order value in minor units |
   220→| `visitorsCount` | `int` | Unique visitors in range |
   221→| `salesChange` | `float` | Percentage change vs previous period |
   222→| `ordersChange` | `float` | Percentage change vs previous period |
   223→| `aovChange` | `float` | Percentage change vs previous period |
   224→| `visitorsChange` | `float` | Percentage change vs previous period |
   225→| `ordersChartData` | `array` | Array of `{date, count}` for last 30 days |
   226→| `topProducts` | `array` | Top 5 products: `{title, units_sold, revenue}` |
   227→| `funnelData` | `array` | Funnel steps: `{visits, add_to_cart, checkout_started, checkout_completed}` |
   228→
   229→| Action | Behavior |
   230→|--------|----------|
   231→| `updatedDateRange()` | Re-fetches all KPI data for the selected range |
   232→| `loadKpis()` | Queries aggregated order/analytics data |
   233→| `loadChart()` | Queries daily order counts for chart |
   234→| `loadTopProducts()` | Queries top 5 products by revenue |
   235→| `loadFunnel()` | Queries conversion funnel data |
   236→
   237→| Computed Property | Purpose |
   238→|-------------------|---------|
   239→| `formattedTotalSales` | Formatted currency string |
   240→| `formattedAov` | Formatted currency string |
   241→
   242→### Wireframe
   243→
   244→```
   245→+------------------------------------------------------------------+
   246→| Dashboard                           [Today v] (date range picker) |
   247→+------------------------------------------------------------------+
   248→|                                                                    |
   249→|  +-------------+  +-------------+  +-------------+  +----------+  |
   250→|  | Total Sales |  | Orders      |  | Avg Order   |  | Visitors |  |
   251→|  | $12,345     |  | 156         |  | $79.13      |  | 2,340    |  |
   252→|  | [+12.3%] ^  |  | [+8.1%] ^  |  | [-2.1%] v  |  | [+5%] ^  |  |
   253→|  +-------------+  +-------------+  +-------------+  +----------+  |
   254→|                                                                    |
   255→|  +--------------------------------------------------------------+  |
   256→|  | Orders over time                                              |  |
   257→|  |  [line/bar chart - daily order counts for selected range]     |  |
   258→|  +--------------------------------------------------------------+  |
   259→|                                                                    |
   260→|  +------------------------------+  +----------------------------+  |
   261→|  | Top products                 |  | Conversion funnel          |  |
   262→|  | Product Title | Sold | Rev   |  | Visits       [========] N |  |
   263→|  | Blue Shirt    | 45   | $900  |  | Add to Cart  [======]   N |  |
   264→|  | Red Hat       | 32   | $640  |  | Checkout     [====]     N |  |
   265→|  | Sneakers      | 28   | $2800 |  | Completed    [==]       N |  |
   266→|  | ...           |      |       |  |                            |  |
   267→|  +------------------------------+  +----------------------------+  |
   268→+--------------------------------------------------------------------+
   269→```
   270→
   271→### Layout Description
   272→
   273→**Page heading:** "Dashboard" displayed as a Flux heading (size xl).
   274→
   275→**Date range filter:** Positioned top-right. A Flux dropdown triggered by a button showing the current range label and a calendar icon. Options: Today, Last 7 days, Last 30 days, Custom range (reveals two date input fields when selected).
   276→
   277→### KPI Tiles Row
   278→
   279→A responsive 4-column grid (1 column on mobile, 2 on sm, 4 on lg).
   280→
   281→Each tile is a card containing:
   282→- A muted-color text label (e.g., "Total Sales")
   283→- A large heading with the formatted number value
   284→- A row with a Flux badge showing percentage change (green if positive, red if negative) and a directional arrow icon
   285→
   286→Each tile shows a loading state (reduced opacity) while data refreshes.
   287→
   288→### Orders Chart
   289→
   290→A card below the KPI tiles containing:
   291→- A Flux heading (size lg): "Orders over time"
   292→- A Chart.js canvas (line or bar chart) showing daily order counts
   293→- Chart.js initialized via Alpine.js; data passed from Livewire
   294→- A spinner overlay shown during chart data loading
   295→
   296→### Top Products Table
   297→
   298→A card containing:
   299→- A Flux heading (size lg): "Top products"
   300→- A plain table with columns: Product Title, Units Sold, Revenue
   301→- Maximum 5 rows
   302→- Empty state text: "No sales data for this period."
   303→
   304→### Conversion Funnel
   305→
   306→A card containing:
   307→- A Flux heading (size lg): "Conversion funnel"
   308→- Horizontal bar visualization using Tailwind utility widths
   309→- Each step: label on the left, proportional-width bar on the right, count displayed
   310→- Steps: Visits, Add to Cart, Checkout Started, Checkout Completed
   311→- Bars use decreasing widths and progressively darker colors
   312→
   313→### Flux Component Mapping
   314→
   315→| UI Element | Flux Component |
   316→|-----------|----------------|
   317→| Page title | `heading` (size xl) |
   318→| Section titles | `heading` (size lg) |
   319→| Date range selector | `dropdown`, `button`, `icon` |
   320→| Date inputs (custom range) | `input` (type date) |
   321→| KPI labels | `text` |
   322→| KPI values | `heading` (size xl) |
   323→| Trend badges | `badge` |
   324→| Trend arrows | `icon` |
   325→
   326→### View States
   327→
   328→| State | Behavior |
   329→|-------|----------|
   330→| Loading | KPI tiles show reduced opacity; chart shows spinner overlay |
   331→| Empty | "No sales data for this period." in top products; funnel bars at zero width |
   332→| Populated | All tiles, chart, table, and funnel render with data |
   333→| Error | Toast notification with error message |
   334→
   335→---
   336→
   337→## 3. Products List
   338→
   339→**Route:** `GET /admin/products`
   340→
   341→**Livewire component:** `App\Livewire\Admin\Products\Index`
   342→
   343→**Blade view:** `resources/views/livewire/admin/products/index.blade.php`
   344→
   345→### Component Requirements
   346→
   347→| Property | Type | Purpose |
   348→|----------|------|---------|
   349→| `search` | `string` | Search query, live-debounced at 300ms |
   350→| `statusFilter` | `string` | Status filter: `all`, `draft`, `active`, `archived` |
   351→| `typeFilter` | `string` | Product type filter |
   352→| `selectedIds` | `array<int>` | Array of selected product IDs for bulk actions |
   353→| `selectAll` | `bool` | Whether all visible rows are selected |
   354→| `sortField` | `string` | Column to sort by (default: `updated_at`) |
   355→| `sortDirection` | `string` | `asc` or `desc` |
   356→
   357→| Action | Behavior |
   358→|--------|----------|
   359→| `updatedSearch()` | Resets pagination |
   360→| `updatedStatusFilter()` | Resets pagination |
   361→| `updatedTypeFilter()` | Resets pagination |
   362→| `bulkArchive()` | Archives all selected products |
   363→| `bulkDelete()` | Soft-deletes selected products (with confirmation) |
   364→| `bulkSetActive()` | Sets selected products to active status |
   365→| `sortBy(string $field)` | Toggles sort direction or changes sort field |
   366→| `toggleSelectAll()` | Selects or deselects all visible product IDs |
   367→| `confirmBulkDelete()` | Opens delete confirmation modal |
   368→
   369→| Computed Property | Purpose |
   370→|-------------------|---------|
   371→| `products` | Paginated query result with search, filters, sorting applied. Eager loads `variants`, `media` (limit 1). Uses `withCount('variants')`. |
   372→| `productTypes` | Distinct product types for the type filter dropdown |
   373→
   374→### Wireframe
   375→
   376→```
   377→+------------------------------------------------------------------+
   378→| Products                                    [+ Add product]       |
   379→+------------------------------------------------------------------+
   380→| [search icon] Search products...   [Status: All v] [Type: All v] |
   381→+------------------------------------------------------------------+
   382→| (shown when items selected)                                       |
   383→| "3 products selected"  [Set Active]  [Archive]  [Delete]         |
   384→+------------------------------------------------------------------+
   385→| [x] | IMG | Title ^     | Status  | Inventory | Type | Vendor | Updated ^  |
   386→|-----|-----|-------------|---------|-----------|------|--------|-----------|
   387→| [ ] | img | Blue Shirt  | Active  | 45        | Tops | Nike   | 2h ago    |
   388→| [ ] | img | Red Hat     | Draft   | 12        | Hats | Adidas | 1d ago    |
   389→| [ ] | img | Sneakers    | Active  | 0         | Shoes| Nike   | 3d ago    |
   390→| ...                                                              |
   391→+------------------------------------------------------------------+
   392→| < 1 2 3 ... 10 >  (pagination)                                  |
   393→+------------------------------------------------------------------+
   394→```
   395→
   396→### Table Column Definitions
   397→
   398→| Column | Sortable | Content |
   399→|--------|----------|---------|
   400→| Select | No | Checkbox for bulk selection |
   401→| Image | No | 40x40 rounded thumbnail of first media item, or placeholder icon |
   402→| Title | Yes | Product title as a link to the edit page (uses Livewire navigate) |
   403→| Status | No | Flux badge: draft=zinc/gray, active=green, archived=red |
   404→| Inventory | Yes | Sum of `inventory_items.quantity_on_hand` across all variants |
   405→| Type | No | Product type text or "-" |
   406→| Vendor | No | Vendor name text or "-" |
   407→| Updated | Yes (default) | Relative timestamp (e.g., "2h ago") |
   408→
   409→### Bulk Action Bar
   410→
   411→Conditionally shown when `count($selectedIds) > 0`. Contains:
   412→- Text showing "X products selected"
   413→- "Set Active" button (ghost variant)
   414→- "Archive" button (ghost variant)
   415→- "Delete" button (danger variant) - opens confirmation modal
   416→
   417→### Empty State
   418→
   419→When no products exist (not filtered, genuinely empty):
   420→- Centered container with a large muted placeholder icon (cube)
   421→- Heading: "Add your first product"
   422→- Text: "Start building your catalog by adding products."
   423→- Primary button: "Add product" linking to the create page
   424→
   425→### Delete Confirmation Modal
   426→
   427→| Property | Value |
   428→|----------|-------|
   429→| Modal name | `confirm-bulk-delete` |
   430→| Max width | md |
   431→| Heading | "Delete products?" |
   432→| Body text | "This will archive X product(s). Products with orders cannot be permanently deleted." |
   433→| Cancel button | Ghost variant, closes modal |
   434→| Confirm button | Danger variant, calls `bulkDelete` |
   435→
   436→### Flux Component Mapping
   437→
   438→| UI Element | Flux Component |
   439→|-----------|----------------|
   440→| Page title | `heading` |
   441→| Add product button | `button` (variant: primary) |
   442→| Search field | `input` (icon: magnifying-glass) |
   443→| Status filter | `select` |
   444→| Type filter | `select` |
   445→| Bulk action buttons | `button` (ghost/danger variants) |
   446→| Selected count text | `text` |
   447→| Row checkbox | `checkbox` |
   448→| Status badges | `badge` |
   449→| Cell text | `text` |
   450→| Delete confirmation | `modal` |
   451→| Pagination | Laravel default Tailwind pagination |
   452→
   453→### View States
   454→
   455→| State | Behavior |
   456→|-------|----------|
   457→| Loading | Table body shows reduced opacity |
   458→| Empty (no products) | Empty state with illustration and CTA |
   459→| Empty (filtered) | Table with "No products match your filters" message |
   460→| Populated | Table with data rows and pagination |
   461→
   462→---
   463→
   464→## 4. Product Create/Edit
   465→
   466→**Route:** `GET /admin/products/create` and `GET /admin/products/{product}/edit`
   467→
   468→**Livewire component:** `App\Livewire\Admin\Products\Form`
   469→
   470→**Blade view:** `resources/views/livewire/admin/products/form.blade.php`
   471→
   472→### Component Requirements
   473→
   474→| Property | Type | Purpose |
   475→|----------|------|---------|
   476→| `product` | `?Product` | The product being edited, null for create |
   477→| `title` | `string` | Product title |
   478→| `descriptionHtml` | `string` | Product description (HTML) |
   479→| `status` | `string` | `draft`, `active`, `archived` |
   480→| `vendor` | `string` | Vendor name |
   481→| `productType` | `string` | Product type string |
   482→| `tags` | `string` | Comma-separated tags |
   483→| `handle` | `string` | URL handle/slug |
   484→| `publishedAt` | `?string` | Publish date (Y-m-d H:i) |
   485→| `collectionIds` | `array<int>` | Selected collection IDs |
   486→| `options` | `array` | Options array: `[{name: string, values: string[]}]` |
   487→| `variants` | `array` | Variants array: `[{sku, price, compareAtPrice, quantity, requiresShipping, optionValues}]` |
   488→| `media` | `array` | Existing media items: `[{id, url, alt_text, position}]` |
   489→| `newMedia` | `array` | Temporary uploaded files |
   490→
   491→| Action | Behavior |
   492→|--------|----------|
   493→| `mount(?Product $product)` | Loads product data into properties or sets defaults for create |
   494→| `save()` | Validates and saves/creates the product, variants, options, and media |
   495→| `addOption()` | Adds a new empty option `{name: '', values: []}` |
   496→| `removeOption(int $index)` | Removes an option and regenerates variants |
   497→| `addOptionValue(int $optionIndex)` | Adds a value to an option |
   498→| `removeOptionValue(int $optionIndex, int $valueIndex)` | Removes a value |
   499→| `generateVariants()` | Generates variant combinations from current options |

Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Platform logo and name | `brand` |
| Navigation icons | `icon` |
| Group dividers | `separator` |

#### Responsive Behavior

- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.

### 1.3 Top Bar

**Livewire component:** `App\Livewire\Admin\Layout\TopBar`

**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `currentStoreName` | `string` | Name of the active store |
| `stores` | `Collection` | List of stores the user has access to |
| `unreadNotificationCount` | `int` | Count of unread notifications |

| Action | Behavior |
|--------|----------|
| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |

#### Wireframe

```
+--------------------------------------------------------------------+
| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
+--------------------------------------------------------------------+
  * hamburger visible on mobile only

  Store Name dropdown:
    - Store A
    - Store B
    - Store C

  Avatar dropdown:
    - Settings (links to /admin/settings)
    - [separator]
    - Log out (POST /logout with CSRF)
```

#### Interaction Flows

- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Store selector | `dropdown` |
| Hamburger (mobile) | `button` (icon: bars-3) |
| Notification bell | `button` (icon: bell, variant: ghost) |
| Notification count | `badge` |
| User profile trigger | `profile` |
| Profile menu | `dropdown` |
| Menu divider | `separator` |

### 1.4 Main Content Area

The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.

**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".

**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.

### 1.5 Toast Notifications

A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.

| Property | Value |
|----------|-------|
| Event name | `toast` |
| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
| Position | Top-right corner |
| Auto-dismiss | 5 seconds |
| Stacking | Supports multiple toasts |
| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |

Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").

### 1.6 Dark Mode

The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.

Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.

---

## 2. Dashboard

**Route:** `GET /admin`

**Livewire component:** `App\Livewire\Admin\Dashboard`

**Blade view:** `resources/views/livewire/admin/dashboard.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Active date range preset: `today`, `last_7_days`, `last_30_days`, `custom` |
| `customStartDate` | `?string` | Custom range start (Y-m-d) |
| `customEndDate` | `?string` | Custom range end (Y-m-d) |
| `totalSales` | `int` | Total sales amount in minor units |
| `ordersCount` | `int` | Number of orders in range |
| `averageOrderValue` | `int` | Average order value in minor units |
| `visitorsCount` | `int` | Unique visitors in range |
| `salesChange` | `float` | Percentage change vs previous period |
| `ordersChange` | `float` | Percentage change vs previous period |
| `aovChange` | `float` | Percentage change vs previous period |
| `visitorsChange` | `float` | Percentage change vs previous period |
| `ordersChartData` | `array` | Array of `{date, count}` for last 30 days |
| `topProducts` | `array` | Top 5 products: `{title, units_sold, revenue}` |
| `funnelData` | `array` | Funnel steps: `{visits, add_to_cart, checkout_started, checkout_completed}` |

| Action | Behavior |
|--------|----------|
| `updatedDateRange()` | Re-fetches all KPI data for the selected range |
| `loadKpis()` | Queries aggregated order/analytics data |
| `loadChart()` | Queries daily order counts for chart |
| `loadTopProducts()` | Queries top 5 products by revenue |
| `loadFunnel()` | Queries conversion funnel data |

| Computed Property | Purpose |
|-------------------|---------|
| `formattedTotalSales` | Formatted currency string |
| `formattedAov` | Formatted currency string |

### Wireframe

```
+------------------------------------------------------------------+
| Dashboard                           [Today v] (date range picker) |
+------------------------------------------------------------------+
|                                                                    |
|  +-------------+  +-------------+  +-------------+  +----------+  |
|  | Total Sales |  | Orders      |  | Avg Order   |  | Visitors |  |
|  | $12,345     |  | 156         |  | $79.13      |  | 2,340    |  |
|  | [+12.3%] ^  |  | [+8.1%] ^  |  | [-2.1%] v  |  | [+5%] ^  |  |
|  +-------------+  +-------------+  +-------------+  +----------+  |
|                                                                    |
|  +--------------------------------------------------------------+  |
|  | Orders over time                                              |  |
|  |  [line/bar chart - daily order counts for selected range]     |  |
|  +--------------------------------------------------------------+  |
|                                                                    |
|  +------------------------------+  +----------------------------+  |
|  | Top products                 |  | Conversion funnel          |  |
|  | Product Title | Sold | Rev   |  | Visits       [========] N |  |
|  | Blue Shirt    | 45   | $900  |  | Add to Cart  [======]   N |  |
|  | Red Hat       | 32   | $640  |  | Checkout     [====]     N |  |
|  | Sneakers      | 28   | $2800 |  | Completed    [==]       N |  |
|  | ...           |      |       |  |                            |  |
|  +------------------------------+  +----------------------------+  |
+--------------------------------------------------------------------+
```

### Layout Description

**Page heading:** "Dashboard" displayed as a Flux heading (size xl).

**Date range filter:** Positioned top-right. A Flux dropdown triggered by a button showing the current range label and a calendar icon. Options: Today, Last 7 days, Last 30 days, Custom range (reveals two date input fields when selected).

### KPI Tiles Row

A responsive 4-column grid (1 column on mobile, 2 on sm, 4 on lg).

Each tile is a card containing:
- A muted-color text label (e.g., "Total Sales")
- A large heading with the formatted number value
- A row with a Flux badge showing percentage change (green if positive, red if negative) and a directional arrow icon

Each tile shows a loading state (reduced opacity) while data refreshes.

### Orders Chart

A card below the KPI tiles containing:
- A Flux heading (size lg): "Orders over time"
- A Chart.js canvas (line or bar chart) showing daily order counts
- Chart.js initialized via Alpine.js; data passed from Livewire
- A spinner overlay shown during chart data loading

### Top Products Table

A card containing:
- A Flux heading (size lg): "Top products"
- A plain table with columns: Product Title, Units Sold, Revenue
- Maximum 5 rows
- Empty state text: "No sales data for this period."

### Conversion Funnel

A card containing:
- A Flux heading (size lg): "Conversion funnel"
- Horizontal bar visualization using Tailwind utility widths
- Each step: label on the left, proportional-width bar on the right, count displayed
- Steps: Visits, Add to Cart, Checkout Started, Checkout Completed
- Bars use decreasing widths and progressively darker colors

### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Page title | `heading` (size xl) |
| Section titles | `heading` (size lg) |
| Date range selector | `dropdown`, `button`, `icon` |
| Date inputs (custom range) | `input` (type date) |
| KPI labels | `text` |
| KPI values | `heading` (size xl) |
| Trend badges | `badge` |
| Trend arrows | `icon` |

### View States

| State | Behavior |
|-------|----------|
| Loading | KPI tiles show reduced opacity; chart shows spinner overlay |
| Empty | "No sales data for this period." in top products; funnel bars at zero width |
| Populated | All tiles, chart, table, and funnel render with data |
| Error | Toast notification with error message |

---

## 3. Products List

**Route:** `GET /admin/products`

**Livewire component:** `App\Livewire\Admin\Products\Index`

**Blade view:** `resources/views/livewire/admin/products/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `search` | `string` | Search query, live-debounced at 300ms |
| `statusFilter` | `string` | Status filter: `all`, `draft`, `active`, `archived` |
| `typeFilter` | `string` | Product type filter |
| `selectedIds` | `array<int>` | Array of selected product IDs for bulk actions |
| `selectAll` | `bool` | Whether all visible rows are selected |
| `sortField` | `string` | Column to sort by (default: `updated_at`) |
| `sortDirection` | `string` | `asc` or `desc` |

| Action | Behavior |
|--------|----------|
| `updatedSearch()` | Resets pagination |
| `updatedStatusFilter()` | Resets pagination |
| `updatedTypeFilter()` | Resets pagination |
| `bulkArchive()` | Archives all selected products |
| `bulkDelete()` | Soft-deletes selected products (with confirmation) |
| `bulkSetActive()` | Sets selected products to active status |
| `sortBy(string $field)` | Toggles sort direction or changes sort field |
| `toggleSelectAll()` | Selects or deselects all visible product IDs |
| `confirmBulkDelete()` | Opens delete confirmation modal |

| Computed Property | Purpose |
|-------------------|---------|
| `products` | Paginated query result with search, filters, sorting applied. Eager loads `variants`, `media` (limit 1). Uses `withCount('variants')`. |
| `productTypes` | Distinct product types for the type filter dropdown |

### Wireframe

```
+------------------------------------------------------------------+
| Products                                    [+ Add product]       |
+------------------------------------------------------------------+
| [search icon] Search products...   [Status: All v] [Type: All v] |
+------------------------------------------------------------------+
| (shown when items selected)                                       |
| "3 products selected"  [Set Active]  [Archive]  [Delete]         |
+------------------------------------------------------------------+
| [x] | IMG | Title ^     | Status  | Inventory | Type | Vendor | Updated ^  |
|-----|-----|-------------|---------|-----------|------|--------|-----------|
| [ ] | img | Blue Shirt  | Active  | 45        | Tops | Nike   | 2h ago    |
| [ ] | img | Red Hat     | Draft   | 12        | Hats | Adidas | 1d ago    |
| [ ] | img | Sneakers    | Active  | 0         | Shoes| Nike   | 3d ago    |
| ...                                                              |
+------------------------------------------------------------------+
| < 1 2 3 ... 10 >  (pagination)                                  |
+------------------------------------------------------------------+
```

### Table Column Definitions

| Column | Sortable | Content |
|--------|----------|---------|
| Select | No | Checkbox for bulk selection |
| Image | No | 40x40 rounded thumbnail of first media item, or placeholder icon |
| Title | Yes | Product title as a link to the edit page (uses Livewire navigate) |
| Status | No | Flux badge: draft=zinc/gray, active=green, archived=red |
| Inventory | Yes | Sum of `inventory_items.quantity_on_hand` across all variants |
| Type | No | Product type text or "-" |
| Vendor | No | Vendor name text or "-" |
| Updated | Yes (default) | Relative timestamp (e.g., "2h ago") |

### Bulk Action Bar

Conditionally shown when `count($selectedIds) > 0`. Contains:
- Text showing "X products selected"
- "Set Active" button (ghost variant)
- "Archive" button (ghost variant)
- "Delete" button (danger variant) - opens confirmation modal

### Empty State

When no products exist (not filtered, genuinely empty):
- Centered container with a large muted placeholder icon (cube)
- Heading: "Add your first product"
- Text: "Start building your catalog by adding products."
- Primary button: "Add product" linking to the create page

### Delete Confirmation Modal

| Property | Value |
|----------|-------|
| Modal name | `confirm-bulk-delete` |
| Max width | md |
| Heading | "Delete products?" |
| Body text | "This will archive X product(s). Products with orders cannot be permanently deleted." |
| Cancel button | Ghost variant, closes modal |
| Confirm button | Danger variant, calls `bulkDelete` |

### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Page title | `heading` |
| Add product button | `button` (variant: primary) |
| Search field | `input` (icon: magnifying-glass) |
| Status filter | `select` |
| Type filter | `select` |
| Bulk action buttons | `button` (ghost/danger variants) |
| Selected count text | `text` |
| Row checkbox | `checkbox` |
| Status badges | `badge` |
| Cell text | `text` |
| Delete confirmation | `modal` |
| Pagination | Laravel default Tailwind pagination |

### View States

| State | Behavior |
|-------|----------|
| Loading | Table body shows reduced opacity |
| Empty (no products) | Empty state with illustration and CTA |
| Empty (filtered) | Table with "No products match your filters" message |
| Populated | Table with data rows and pagination |

---

## 4. Product Create/Edit

**Route:** `GET /admin/products/create` and `GET /admin/products/{product}/edit`

**Livewire component:** `App\Livewire\Admin\Products\Form`

**Blade view:** `resources/views/livewire/admin/products/form.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `product` | `?Product` | The product being edited, null for create |
| `title` | `string` | Product title |
| `descriptionHtml` | `string` | Product description (HTML) |
| `status` | `string` | `draft`, `active`, `archived` |
| `vendor` | `string` | Vendor name |
| `productType` | `string` | Product type string |
| `tags` | `string` | Comma-separated tags |
| `handle` | `string` | URL handle/slug |
| `publishedAt` | `?string` | Publish date (Y-m-d H:i) |
| `collectionIds` | `array<int>` | Selected collection IDs |
| `options` | `array` | Options array: `[{name: string, values: string[]}]` |
| `variants` | `array` | Variants array: `[{sku, price, compareAtPrice, quantity, requiresShipping, optionValues}]` |
| `media` | `array` | Existing media items: `[{id, url, alt_text, position}]` |
| `newMedia` | `array` | Temporary uploaded files |

| Action | Behavior |
|--------|----------|
| `mount(?Product $product)` | Loads product data into properties or sets defaults for create |
| `save()` | Validates and saves/creates the product, variants, options, and media |
| `addOption()` | Adds a new empty option `{name: '', values: []}` |
| `removeOption(int $index)` | Removes an option and regenerates variants |
| `addOptionValue(int $optionIndex)` | Adds a value to an option |
| `removeOptionValue(int $optionIndex, int $valueIndex)` | Removes a value |
| `generateVariants()` | Generates variant combinations from current options |