---
name: fluent-retailer-config
description: Configure Fluent Commerce retailer environments — create locations, networks, catalogues, inventory, carriers, and users via GraphQL. Day-0 setup for new retailers and environment cloning. Triggers on "setup retailer", "create location", "create network", "setup inventory", "retailer config", "environment setup".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [--retailer <ref>] [--setup-type full|locations|networks|catalogues|inventory]
cursor-tier: manual
planning-gate: advisory
---

# Retailer Environment Configuration

Configure Fluent Commerce retailer environments from empty to workflow-ready. Create locations, networks, catalogues, products, inventory, carriers, and users via GraphQL mutations and batch ingestion. This is the "Day 0" skill for new retailers and environment cloning.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-retailer-config.jsonl` exists
2. If yes, read last 20 records
3. Filter to: FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Planning Gate

### Pre-flight: Plan Verification

Before proceeding, check for an existing approved plan:

1. **Search** `accounts/<PROFILE>/features/*/status.json` for an entry with `plan: "APPROVED"` that covers this retailer configuration, OR check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
2. **If multi-artifact work** (retailer config + rules + workflows): STOP. Invoke `/fluent-feature-plan` first — this skill cannot be used for multi-artifact work without a feature plan
3. **If approved plan found:** Skip to implementation, referencing the plan's relevant sections
4. **If retailer-config-only work with no plan:** Continue to the Planning Gate below to write a plan for this skill

**Before creating any entities or modifying retailer configuration, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.**

**Retailer-config specific emphasis — ensure these are covered:**

1. **Business Context (Section 1)** — why this configuration is needed (Day-0 setup, new feature, environment cloning)
2. **Entity relationship diagram (Section 3)** — Mermaid diagram showing entities to create and their dependencies (locations, networks, catalogues, inventory). Validate syntax per `knowledge/mermaid-syntax-rules.md`
3. **Impacted retailers (Section 4.8)** — profile, retailer ref, retailer ID, environment URL
4. **GraphQL operations (Section 4.7)** — list of create/update mutations and batch operations to execute
5. **Detailed Design (Section 5)** — entities to create with refs, types, and key attributes (e.g., location: ref, name, type, openingSchedule)
6. **Deployment Sequence (Section 9)** — order of creation respecting dependencies (locations before networks, catalogues before inventory, products before positions)
7. **Rollback Plan (Section 10)** — note that entity creation is not easily reversible in Fluent; list what can be soft-deleted vs what persists

**Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-retailer-config-<slug>.md`. Set `Status: PENDING`.

Present the full plan content to the user and wait for approval before sending any create/update mutations. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).

## Pre-flight: GraphQL Validation Gate (MANDATORY)

See `knowledge/platform/graphql-preflight.md`.

This skill creates entities via GraphQL mutations. Before executing ANY mutation:

1. **Validate** via `graphql.validate({ query: "<mutation-string>" })` — check the `valid` field (NOT `ok`)
2. **Or introspect** via `graphql.introspect({ mutation: "<mutationName>" })` to discover required input fields
3. If `valid: false`, fix before executing — a mutation with wrong input fields will fail at runtime

This gate also applies to discovery queries used in the Full Setup Recipe (Step 2 onwards).

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Entities created/updated | `-> READY: Location LOC_WAREHOUSE_01 created for retailer <RETAILER_REF>` |
| `-> NEXT: /fluent-<skill>` | Next skill in plan | `-> NEXT: /fluent-settings` |
| `-> BLOCKED: <reason>` | Cannot proceed | `-> BLOCKED: ENV_UNREACHABLE — Cannot connect to Fluent environment` |

### Error codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PLAN_REQUIRED` | Entity creation attempted without approved plan | Run `/fluent-feature-plan` first |
| `ENV_UNREACHABLE` | Cannot connect to Fluent environment | Check credentials via `config.validate` |
| `VALIDATION_FAILED` | Entity data invalid or duplicate detected | Fix input data and retry |

## Pre-flight: Environment Validation

Before creating any entities, validate the environment is accessible and safe:

1. **Verify profile connectivity:**
   ```
   config.validate
   ```
   If validation fails → STOP. Credentials are invalid or expired.

2. **Verify retailer exists:**
   ```
   environment.discover include=retailer
   ```
   If retailer not found → STOP. Create retailer first via `/fluent-cli-retailer` or `/fluent-bootstrap`.

3. **Detect production environment:**
   Check the base URL from the profile. If it does NOT contain `sandbox`, `test`, `dev`, `uat`, or `staging`:
   - **WARN:** *"This appears to be a PRODUCTION environment. Retailer configuration changes are not easily reversible."*
   - Require explicit user confirmation: *"I confirm this is intended for production"*
   - If the user does not confirm → STOP

This check prevents mutations against wrong environments and ensures the target retailer exists.

## Ownership Boundary

This skill owns retailer entity configuration: locations, networks, catalogues, products, inventory positions, virtual catalogues, carriers, and users.

Environment discovery (read-only queries) is owned by `/fluent-test-data`.
Settings management is owned by `/fluent-settings`.
Canonical MCP extension tool syntax/limits are owned by `/fluent-mcp-tools`.

### Module Name Convention

Module name for user action `modules[]` values is determined by each app's `orchestrationAlias` property in its root manifest (NOT the manifest setting name suffix). Standard values: `"adminconsole"` (OMS App — manifest setting is `fc.mystique.manifest.oms`), `"servicepoint"` (Store App — manifest setting is `fc.mystique.manifest.store`). Custom apps use their `orchestrationAlias` or default to the manifest setting suffix (e.g., `fc.mystique.manifest.custom` → `"custom"`).

## When to Use

- Setting up a brand new retailer from scratch
- Adding new locations (warehouses, stores, DCs) to an existing retailer
- Creating fulfilment networks and linking locations
- Setting up product catalogues and inventory for testing
- Cloning an environment's entity structure to another retailer
- Pre-go-live environment preparation
- Creating carriers for shipment tracking
- Provisioning users and assigning roles

## Configuration Sequence

Entities have dependencies. They MUST be created in this order:

| Step | Entity | Dependencies |
|------|--------|-------------|
| 1 | Retailer | None (usually already exists) |
| 2 | Locations (warehouses, stores, DCs) | Retailer |
| 3 | Networks | Retailer, Locations |
| 4 | Product Catalogues | Retailer |
| 5 | Products (Standard then Variant) | Product Catalogue |
| 6 | Inventory Positions + Quantities | Products, Locations |
| 7 | Virtual Catalogues + Virtual Positions | Products, Catalogues |
| 8 | Carriers | Retailer |
| 9 | Customers | Retailer |
| 10 | Users + Roles | Retailer |

**Do not skip steps.** Creating inventory before products exist will fail. Creating networks before locations exist means there is nothing to link.

### Schema Patterns (Global Inventory Model)

> **WARNING:** The Fluent Global Inventory model changes how entities are scoped. Products and inventory use `catalogue: { ref }` instead of `retailer: { id }`. No create inputs have a `status` field — status is auto-set to `CREATED` and advances via workflow events. Always use `graphql.introspect` to verify field names against the live schema.

| Entity | Scoping Field | Required Fields (non-obvious) |
|--------|--------------|-------------------------------|
| Location | `retailer: { id }` | `openingSchedule` (always required), `primaryAddress.ref`, `.latitude`, `.longitude` |
| Network | `retailers: [{ id }]` (array) | `name` (becomes ref), NO `ref` field |
| Product Catalogue | `retailerRefs: ["ref"]` (array of strings) | NO `retailer: { id }` |
| Standard Product | `catalogue: { ref }` | `type`, `gtin` (both required, gtin max 20 chars), NO `retailer`, NO `status` |
| Variant Product | `catalogue: { ref }` | `type`, `gtin` (max 20 chars), `product: { ref, catalogue: { ref } }` (compound key, NOT `standardProduct`), NO `retailer`, NO `status` |
| Inventory Position | `catalogue: { ref }` (InventoryCatalogueKey) | NO `retailer`, NO `status` |
| Virtual Catalogue | `retailerRefs: ["ref"]` (optional) | `inventoryCatalogueRef`, `productCatalogueRef` (both required), NO `retailer: { id }`, NO `status` |
| Virtual Position | `catalogue: { ref }` (VirtualCatalogueKey) | `quantity` (Int!, required), NO `retailer`, NO `status` |
| Carrier | `retailer: { id }` | NO `status` in create input |
| Customer | `retailer: { id }` | `promotionOptIn` (Boolean!, required), NO `ref` field, `username` is the identifier |

## Pre-flight Discovery Protocol

**MANDATORY: Run this discovery before ANY entity creation.** This populates the environment context variables used in all subsequent mutations. Skipping this leads to wrong catalogue refs, missing required fields, and silent failures.

### Step 1: Identify the Account (from config, not GraphQL)

The Fluent **account name** is NOT available via GraphQL. Extract it from the base URL returned by `config.validate`:

```
Base URL pattern: https://<account>[.<environment>].api.fluentretail.com
```

| Base URL | Account | Environment |
|----------|---------|-------------|
| `https://example-account.test.api.fluentretail.com` | `example-account` | **test** |
| `https://example-account.sandbox.api.fluentretail.com` | `example-account` | **sandbox** |
| `https://example-account.api.fluentretail.com` | `example-account` | **production** (no qualifier) |
| `https://another-account.sandbox.api.fluentretail.com` | `another-account` | **sandbox** |

**Rule:** First segment before `.api.fluentretail.com` is the account. If `.test.` or `.sandbox.` is present, that's the environment tier. If neither is present, it's **production** — proceed with extra caution.

### Step 2: Discovery Query (run once, cache results)

```graphql
{
  me { id username primaryRetailer { id ref tradingName status } }
  locations(first: 5) { edges { node { id ref name type status } } pageInfo { hasNextPage } }
  networks(first: 10) { edges { node { id ref type status } } }
  productCatalogues(first: 5) { edges { node { id ref type status } } }
  inventoryCatalogues(first: 5) { edges { node { id ref type status } } }
  virtualCatalogues(first: 5) { edges { node { id ref type status } } }
}
```

> **NOTE:** The User type has `primaryRetailer`, NOT `retailer`. Using `me { retailer { ... } }` will fail with "Field 'retailer' in type 'User' is undefined".

### Required Context Variables

From the discovery response and config, extract and store these variables. **All subsequent mutations reference them.**

| Variable | Source | Example | Used By |
|----------|--------|---------|---------|
| `accountName` | `config.validate` → baseUrl first segment | `"example-account"` | Logging, rule name prefixes |
| `environment` | `config.validate` → baseUrl qualifier | `"test"` / `"sandbox"` / `"production"` | Safety checks |
| `retailerId` | `me.primaryRetailer.id` | `"2"` | Location, Network, Carrier, Customer |
| `retailerRef` | `me.primaryRetailer.ref` | `"RETAILER_REF"` | Product Catalogue (`retailerRefs`), Virtual Catalogue |
| `productCatalogueRef` | `productCatalogues.edges[0].node.ref` | `"PC:MASTER:2"` | Products (`catalogue`), Virtual Catalogue |
| `inventoryCatalogueRef` | `inventoryCatalogues.edges[0].node.ref` | `"DEFAULT:2"` | Inventory Positions (`catalogue`), Virtual Catalogue |
| `existingLocations` | `locations.edges[*].node` | array | Network creation (location IDs) |
| `existingNetworks` | `networks.edges[*].node` | array | Avoid duplicates |

### Validation Checklist (before first mutation)

- [ ] `config.validate()` returns `ok: true` — capture `baseUrl` for account/environment
- [ ] `connection.test()` returns `ok: true`
- [ ] `environment` identified — if **production**, require explicit user confirmation before writes
- [ ] `retailerId` is captured (Int, used as string in `retailer: { id }`)
- [ ] `retailerRef` is captured (used in `retailerRefs` arrays)
- [ ] `productCatalogueRef` exists — if empty, create one first (Step 4)
- [ ] `inventoryCatalogueRef` exists — if empty, account may need Global Inventory enabled
- [ ] No duplicate refs — check `existingLocations` before creating new ones

## Input Validation Rules

**Run these checks on EVERY mutation input before sending.** These constraints are enforced by the GraphQL schema and produce cryptic errors if violated.

### Per-Entity Validation

**Location:**
- `openingSchedule` MUST be present (even for 24/7, use `allHours: true` with all zeros)
- `primaryAddress.ref` MUST be present and unique
- `primaryAddress.latitude` and `.longitude` MUST be present (Float, not String)
- `retailer.id` must be a String: `{ "id": "2" }` not `{ "id": 2 }`

**Network:**
- Use `name` — there is NO `ref` field. The `name` value becomes the `ref` in the response
- `retailers` is an array: `[{ "id": 2 }]` (note: Int here, not String)
- `locations` is optional but recommended at creation: `[{ "id": 134 }]` (Int)

**Products (Standard + Variant):**
- `gtin` max **20 characters** — if product ref > 20 chars, strip underscores or generate shorter code
- `type` is required (e.g., `"STANDARD"`, `"VARIANT"`)
- NO `retailer` field — scope via `catalogue: { ref: "<productCatalogueRef>" }`
- NO `status` field — auto-set to `CREATED`
- Variant `product` is a **compound key**: `{ "ref": "<parentRef>", "catalogue": { "ref": "<productCatalogueRef>" } }`
- Variant `product` field name is `product`, NOT `standardProduct`

**Inventory Position:**
- `catalogue` uses `InventoryCatalogueKey`: `{ "ref": "<inventoryCatalogueRef>" }`
- NO `retailer` field, NO `status` field
- `ref` + `locationRef` combination must be unique within the catalogue

**Virtual Catalogue:**
- `inventoryCatalogueRef` (String!) and `productCatalogueRef` (String!) are BOTH required
- Uses `retailerRefs: ["<retailerRef>"]` (array of strings), NOT `retailer: { id }`
- NO `status` field

**Virtual Position:**
- `quantity` (Int!) is required — use `0` on creation
- `catalogue` uses `VirtualCatalogueKey`: `{ "ref": "<virtualCatalogueRef>" }`

**Customer:**
- NO `ref` field — `username` is the identifier
- `promotionOptIn` (Boolean!) is required — use `false` for test customers
- `firstName` and `lastName` are optional (despite appearing essential)

**Carrier:**
- NO `status` field — auto-set to `CREATED`

### Auto-Fix Patterns

When building mutation inputs, apply these transformations automatically:

```
gtin_value = product_ref.replace("_", "").slice(0, 20)
    if len(gtin_value) > 20: gtin_value = gtin_value[:20]

variant_product_link = {
    "ref": parent_product_ref,
    "catalogue": { "ref": discovered.productCatalogueRef }
}

inventory_catalogue_key = { "ref": discovered.inventoryCatalogueRef }

opening_schedule_24x7 = {
    "allHours": true,
    "monStart": 0, "monEnd": 0, "tueStart": 0, "tueEnd": 0,
    "wedStart": 0, "wedEnd": 0, "thuStart": 0, "thuEnd": 0,
    "friStart": 0, "friEnd": 0, "satStart": 0, "satEnd": 0,
    "sunStart": 0, "sunEnd": 0
}
```

### Ref Collision Prevention

Before creating any entity, query for existing refs to avoid duplicate errors:

```graphql
{ locations(first: 1, ref: ["<proposed_ref>"]) { edges { node { id ref } } } }
```

If edges is non-empty, the ref already exists. Either:
1. Append a timestamp suffix: `<ref>_<YYYYMMDD_HHmmss>`
2. Use the existing entity's ID for subsequent operations

## Step 1: Verify Connectivity and Retailer

Run the Pre-flight Discovery Protocol above. If you have already run discovery, skip to Step 2.

**Quick connectivity check:**
```
connection.test()
```

**Full discovery (captures all context variables):**

Use the discovery query from the Pre-flight section. Capture `retailerId`, `retailerRef`, `productCatalogueRef`, `inventoryCatalogueRef`, and existing entity lists.

## Step 2: Location Management

Locations are physical or logical sites where inventory is held and orders are fulfilled. Every fulfilment workflow depends on locations existing first.

### Location Types

| Type | Purpose | Typical Use |
|------|---------|-------------|
| `WAREHOUSE` | Centralized distribution centre | HD (home delivery) sourcing |
| `STORE` | Retail store | CC (click & collect), SFS (ship from store) |
| `DC` | Distribution centre (alias) | Same as WAREHOUSE in some configurations |

### Create Location (Warehouse)

```graphql
mutation($input: CreateLocationInput!) {
  createLocation(input: $input) {
    id
    ref
    name
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "LOC_WH_SYDNEY_01",
    "name": "Sydney Warehouse",
    "type": "WAREHOUSE",
    "supportPhoneNumber": "+61200000001",
    "primaryAddress": {
      "ref": "LOC_WH_SYDNEY_01_ADDR",
      "name": "Sydney Warehouse",
      "street": "100 Kent Street",
      "city": "Sydney",
      "state": "NSW",
      "postcode": "2000",
      "country": "AU",
      "latitude": -33.8688,
      "longitude": 151.2093
    },
    "retailer": { "id": "<retailerId>" },
    "openingSchedule": {
      "allHours": true,
      "monStart": 0, "monEnd": 0,
      "tueStart": 0, "tueEnd": 0,
      "wedStart": 0, "wedEnd": 0,
      "thuStart": 0, "thuEnd": 0,
      "friStart": 0, "friEnd": 0,
      "satStart": 0, "satEnd": 0,
      "sunStart": 0, "sunEnd": 0
    }
  }
}
```

**Note:** `openingSchedule` is **required** on `CreateLocationInput`. For 24/7 warehouses, use `allHours: true` with all day values set to `0`. Day start/end values are integers representing minutes from midnight (e.g., `540` = 09:00, `1080` = 18:00).

### Create Location (Store)

Same mutation, different type and schedule:

```json
{
  "input": {
    "ref": "LOC_STORE_MELB_01",
    "name": "Melbourne CBD Store",
    "type": "STORE",
    "supportPhoneNumber": "+61300000001",
    "primaryAddress": {
      "ref": "LOC_STORE_MELB_01_ADDR",
      "name": "Melbourne CBD Store",
      "street": "200 Bourke Street",
      "city": "Melbourne",
      "state": "VIC",
      "postcode": "3000",
      "country": "AU",
      "latitude": -37.8136,
      "longitude": 144.9631
    },
    "retailer": { "id": "<retailerId>" },
    "openingSchedule": {
      "allHours": false,
      "monStart": 540, "monEnd": 1080,
      "tueStart": 540, "tueEnd": 1080,
      "wedStart": 540, "wedEnd": 1080,
      "thuStart": 540, "thuEnd": 1080,
      "friStart": 540, "friEnd": 1080,
      "satStart": 600, "satEnd": 1020,
      "sunStart": 0, "sunEnd": 0
    }
  }
}
```

### Update Location

```graphql
mutation($input: UpdateLocationInput!) {
  updateLocation(input: $input) {
    id
    ref
    name
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "id": "<location_id>",
    "status": "INACTIVE"
  }
}
```

### Location Notes

- **Address is critical** — latitude and longitude are required for distance-based fulfilment routing. Orders will not route correctly to locations without coordinates.
- **Status lifecycle:** `ACTIVE` (available for fulfilment) or `INACTIVE` (excluded from routing).
- **Refs must be unique** across the retailer. Use a consistent naming convention like `LOC_<TYPE>_<CITY>_<SEQ>`.
- **Use `graphql.introspect` with `type: "CreateLocationInput"`** to discover exact fields for your Fluent version.

## Step 3: Network Management

Networks group locations for fulfilment routing. HD orders route to HD network locations, CC orders route to CC network locations. Without networks, orders have no routing targets.

### Create Network

```graphql
mutation($input: CreateNetworkInput!) {
  createNetwork(input: $input) {
    id
    ref
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "name": "NETWORK_HD",
    "type": "HD",
    "retailers": [{ "id": <retailerId> }],
    "locations": [{ "id": <warehouseLocationId> }]
  }
}
```

**Note:** `CreateNetworkInput` uses `name` (not `ref`) and `retailers` (array, not singular `retailer`). The `name` value becomes the network's `ref` in the response. Include `locations` to link locations at creation time.

For click & collect:
```json
{
  "input": {
    "name": "NETWORK_CC",
    "type": "CC",
    "retailers": [{ "id": <retailerId> }],
    "locations": [{ "id": <storeLocationId> }]
  }
}
```

### Link Locations to Networks

Locations are associated with networks so the routing engine knows which locations serve which fulfilment types. The typical pattern is to update the location with a network reference, or to use the network's `locations` connection.

**Option A — Update location with network assignment:**

Use `graphql.introspect` with `type: "UpdateLocationInput"` to check if your schema supports a `networks` or `networkRef` field. The exact mechanism varies by Fluent version. Common patterns:

```graphql
mutation($input: UpdateLocationInput!) {
  updateLocation(input: $input) {
    id
    ref
    networks {
      edges {
        node { id ref }
      }
    }
  }
}
```

**Option B — Batch ingestion for bulk assignment:**

For assigning many locations to a network at once, use the batch ingestion tools:

```
batch.create({
  "name": "Link locations to HD network",
  "entityType": "LOCATION",
  "action": "UPSERT"
})
```

Then send records with network references via `batch.send`.

**Option C — Attributes-based linkage:**

Some Fluent configurations use attributes on the location entity to declare network membership. Check the workflow and routing rules to determine which approach your account uses.

### Network Notes

- **One network per fulfilment type** is the standard pattern (one HD, one CC, one SFS).
- **Network-location linkage is mandatory** — orders will not route to locations that are not assigned to the appropriate network.
- **Always verify linkage** after creation by querying the location's networks connection.

## Step 4: Catalogue Management

Product catalogues are containers for products. You need at least one catalogue before you can create any products.

### Create Product Catalogue

```graphql
mutation($input: CreateProductCatalogueInput!) {
  createProductCatalogue(input: $input) {
    id
    ref
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "PC:MASTER:<RETAILER_REF>",
    "type": "MASTER",
    "name": "Master Product Catalogue",
    "retailerRefs": ["<RETAILER_REF>"]
  }
}
```

**Note:** `CreateProductCatalogueInput` uses `retailerRefs` (array of retailer ref strings), not `retailer: { id }`. There is no `status` field — catalogues start in `CREATED` status and advance via workflow events.

### Catalogue Notes

- **Ref format convention:** `PC:MASTER:<retailer_ref>` or `PC:<TYPE>:<retailer_ref>`. Query existing catalogues first to match the naming convention.
- **Types:** `MASTER` is the primary catalogue. Some accounts use additional types like `SEASONAL` or `MARKDOWN`.
- **One master catalogue per retailer** is the standard pattern.

## Step 5: Product Management

Products follow a two-level hierarchy: Standard Products (parents) contain Variant Products (children). A Standard Product is a product family (e.g., "T-Shirt"), and Variant Products represent purchasable SKUs (e.g., "T-Shirt Size M Blue").

### Create Standard Product

```graphql
mutation($input: CreateStandardProductInput!) {
  createStandardProduct(input: $input) {
    id
    ref
    name
    status
    catalogue { ref }
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "PROD_TSHIRT_001",
    "type": "STANDARD",
    "gtin": "PROD_TSHIRT_001",
    "name": "Classic T-Shirt",
    "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" }
  }
}
```

### Create Variant Product

```graphql
mutation($input: CreateVariantProductInput!) {
  createVariantProduct(input: $input) {
    id
    ref
    name
    status
    catalogue { ref }
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "SKU_TSHIRT_001_M_BLU",
    "type": "VARIANT",
    "gtin": "SKU_TSHIRT_001_M_BLU",
    "name": "Classic T-Shirt - Medium Blue",
    "product": { "ref": "PROD_TSHIRT_001", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } },
    "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" }
  }
}
```

### Product Notes

- **Global Inventory model**: Products and inventory use `catalogue: { ref }` scoping instead of `retailer: { id }`. There is NO `retailer` field on product or inventory create inputs.
- **`type` and `gtin` are required** on both Standard and Variant products. `gtin` has a **20-character limit** — if your product ref exceeds 20 chars, generate a shorter gtin (e.g., strip underscores or use a numeric code).
- **`status` is auto-set to `CREATED`** on creation — there is no `status` field in the create input. Status advances via workflow events.
- **Variant → parent link** uses `product` (type `ProductKey`), which is a **compound key** requiring BOTH `ref` AND `catalogue: { ref }`. Example: `"product": { "ref": "PROD_001", "catalogue": { "ref": "PC:MASTER:2" } }`. Using just `{ ref }` without the nested catalogue will fail.
- **Standard Products are parents, Variant Products are children.** You must create the Standard Product before its Variants.
- **Product ref = SKU ref** used in order items. The variant product ref is what appears in `productRef` on order line items.
- **Use `graphql.batchMutate`** for creating multiple products:
  ```json
  {
    "mutation": "createVariantProduct",
    "inputs": [
      { "ref": "SKU_001_S_RED", "type": "VARIANT", "gtin": "SKU001SRED", "name": "T-Shirt Small Red", "product": { "ref": "PROD_001", "catalogue": { "ref": "PC:MASTER:RETAILER" } }, "catalogue": { "ref": "PC:MASTER:RETAILER" } },
      { "ref": "SKU_001_M_RED", "type": "VARIANT", "gtin": "SKU001MRED", "name": "T-Shirt Medium Red", "product": { "ref": "PROD_001", "catalogue": { "ref": "PC:MASTER:RETAILER" } }, "catalogue": { "ref": "PC:MASTER:RETAILER" } }
    ],
    "returnFields": ["id", "ref", "name", "status"]
  }
  ```

## Step 6: Inventory Management

Inventory positions link products to locations with stock quantities. Without inventory, products cannot be allocated to fulfilments.

### Create Inventory Position

```graphql
mutation($input: CreateInventoryPositionInput!) {
  createInventoryPosition(input: $input) {
    id
    ref
    type
    status
    onHand
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "SKU_TSHIRT_001_M_BLU",
    "type": "DEFAULT",
    "catalogue": { "ref": "DEFAULT:<retailerId>" },
    "productRef": "SKU_TSHIRT_001_M_BLU",
    "locationRef": "LOC_WH_SYDNEY_01",
    "onHand": 100
  }
}
```

**Note:** `CreateInventoryPositionInput` uses `catalogue: { ref }` (InventoryCatalogueKey), NOT `retailer: { id }`. The catalogue ref format is typically `DEFAULT:<retailerId>` or `COMPATIBILITY:<retailerId>`. There is no `status` field — status defaults to `CREATED` and advances via workflow. Query existing inventory catalogues to discover the correct ref: `{ inventoryCatalogues(first: 5) { edges { node { id ref type } } } }`.

### Create Inventory Quantity

For more granular inventory control (e.g., setting specific quantity types like SALE, RESERVED):

```graphql
mutation($input: CreateInventoryQuantityInput!) {
  createInventoryQuantity(input: $input) {
    id
    ref
    type
    quantity {
      ... on Count { value }
    }
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "SKU_TSHIRT_001_M_BLU_QTY_SALE",
    "type": "SALE",
    "quantity": 100,
    "inventoryPosition": { "ref": "SKU_TSHIRT_001_M_BLU" },
    "catalogue": { "ref": "DEFAULT:<retailerId>" }
  }
}
```

### Bulk Inventory via Batch Ingestion

For large inventory datasets (100+ positions), use the batch tools:

**Step A — Create batch job:**
```
batch.create({
  "name": "Inventory load - <TIMESTAMP>",
  "entityType": "INVENTORY_POSITION",
  "action": "UPSERT",
  "retailerId": "<retailerId>"
})
```

**Step B — Send records:**
```
batch.send({
  "jobId": "<job_id_from_step_A>",
  "payload": {
    "items": [
      {
        "ref": "SKU_001_S_RED",
        "type": "DEFAULT",
        "status": "ACTIVE",
        "productRef": "SKU_001_S_RED",
        "locationRef": "LOC_WH_SYDNEY_01",
        "onHand": 50
      },
      {
        "ref": "SKU_001_M_RED",
        "type": "DEFAULT",
        "status": "ACTIVE",
        "productRef": "SKU_001_M_RED",
        "locationRef": "LOC_WH_SYDNEY_01",
        "onHand": 75
      }
    ]
  }
})
```

**Step C — Poll status:**
```
batch.status({ "jobId": "<job_id>" })
```

Repeat until terminal status (COMPLETE or FAILED).

**Step D — Check results:**
```
batch.results({ "jobId": "<job_id>" })
```

### Inventory Notes

- **Inventory position ref typically matches the product SKU ref.** The combination of `ref` + `locationRef` must be unique.
- **`onHand`** is the primary stock count used by allocation rules. Products with `onHand: 0` will not be allocated.
- **Use `batch.*` tools for anything over 50 entities.** Batch ingestion is significantly faster and more reliable for large datasets.

## Step 7: Virtual Catalogue Setup

Virtual catalogues provide aggregated views of inventory across multiple locations. They are used by the fulfilment options API and checkout to determine product availability without querying each location individually.

### Create Virtual Catalogue

```graphql
mutation($input: CreateVirtualCatalogueInput!) {
  createVirtualCatalogue(input: $input) {
    id
    ref
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "VC:DEFAULT:<RETAILER_REF>",
    "type": "DEFAULT",
    "name": "Default Virtual Catalogue",
    "inventoryCatalogueRef": "DEFAULT:<retailerId>",
    "productCatalogueRef": "PC:MASTER:<RETAILER_REF>",
    "retailerRefs": ["<RETAILER_REF>"]
  }
}
```

**Note:** `CreateVirtualCatalogueInput` requires `inventoryCatalogueRef` and `productCatalogueRef` (both String!, required). It uses `retailerRefs` (array of ref strings, optional), NOT `retailer: { id }`. There is no `status` field — status defaults to `CREATED`.

### Create Virtual Position

Virtual positions link virtual catalogues to product inventory:

```graphql
mutation($input: CreateVirtualPositionInput!) {
  createVirtualPosition(input: $input) {
    id
    ref
    type
    status
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "VP:SKU_TSHIRT_001_M_BLU",
    "type": "DEFAULT",
    "productRef": "SKU_TSHIRT_001_M_BLU",
    "quantity": 0,
    "catalogue": { "ref": "VC:DEFAULT:<RETAILER_REF>" }
  }
}
```

### Virtual Catalogue Notes

- **Virtual catalogues require both `inventoryCatalogueRef` and `productCatalogueRef`** — these link the virtual catalogue to the underlying inventory and product catalogues.
- **Virtual positions require `quantity` (Int!)** — set to `0` on creation; the workflow/orchestration recalculates the aggregated quantity from underlying inventory.
- **No `retailer: { id }` on virtual entities** — use `retailerRefs` (array of ref strings) on the catalogue, and `catalogue: { ref }` on positions.
- **No `status` field in create inputs** — status defaults to `CREATED` and advances via workflow events.
- **Virtual catalogues are optional** but needed for fulfilment options and checkout APIs.
- **Ref format convention:** `VC:<TYPE>:<retailer_ref>` for catalogues, `VP:<product_ref>` for positions.

## Step 8: Carrier Setup

Carriers represent shipping providers. They are required when shipment tracking is part of the fulfilment workflow.

### Create Carrier

```graphql
mutation($input: CreateCarrierInput!) {
  createCarrier(input: $input) {
    id
    ref
    name
    status
  }
}
```

Variables:
```json
{
  "input": {
    "ref": "CARRIER_DHL",
    "name": "DHL Express",
    "type": "SHIPPING",
    "retailer": { "id": "<retailerId>" }
  }
}
```

**Note:** `CreateCarrierInput` requires `ref`, `name`, `type`, and `retailer`. There is no `status` field — status defaults to `CREATED` and advances via workflow events. Use `graphql.introspect` with `type: "CreateCarrierInput"` to discover all available fields.

## Step 9: Customer Setup

Customer creation is already covered in detail by `/fluent-test-data`. Use that skill for creating test customers. The core mutation for reference:

```graphql
mutation($input: CreateCustomerInput!) {
  createCustomer(input: $input) {
    id
    username
    firstName
    lastName
    primaryEmail
  }
}
```

Variables:
```json
{
  "input": {
    "username": "test-customer-<TIMESTAMP>",
    "firstName": "Test",
    "lastName": "Customer",
    "primaryEmail": "test.customer@example.com",
    "promotionOptIn": false,
    "retailer": { "id": "<retailerId>" }
  }
}
```

## Step 10: User and Role Management

Users and roles control access to the Fluent platform. This is typically managed via the Fluent CLI or admin console, but can also be done via GraphQL.

### Create User

Use `graphql.introspect` with `mutation: "createUser"` to discover the exact input type. Common pattern:

```graphql
mutation($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    ref
    username
    status
    primaryEmail
  }
}
```

### Common Roles

| Role | Purpose |
|------|---------|
| `RETAILER_ADMIN` | Full retailer access |
| `LOCATION_USER` | Location-scoped operations (store staff) |
| `FLUENT_ADMIN` | Platform-level admin (account-scoped) |

### User Management Notes

- **Fluent CLI is often simpler** for user management: `fluent profile create` handles user creation and authentication setup.
- **Roles are account-specific.** Use `graphql.introspect` to discover available role types for your account.
- **Location-scoped users** need both a role assignment and a location assignment to function correctly.

## Full Setup Recipe

A step-by-step recipe for setting up a complete retailer environment from scratch. All 10 steps in one sequence.

### Prerequisites

- Fluent MCP extension connected (`connection.test` returns success)
- Retailer ID known (query via `graphql.query`)

### Step-by-Step

**1. Verify connectivity:**
```
connection.test()
```

**2. Check retailer exists:**
```graphql
{
  retailers(first: 10) {
    edges {
      node { id ref tradingName status }
    }
  }
}
```

Capture `retailerId` from the response.

**3. Create 1 warehouse + 1 store:**

Use `graphql.batchMutate` to create both in one call:
```json
{
  "mutation": "createLocation",
  "inputs": [
    {
      "ref": "LOC_WH_01",
      "name": "Primary Warehouse",
      "type": "WAREHOUSE",
      "primaryAddress": {
        "ref": "LOC_WH_01_ADDR",
        "name": "Primary Warehouse",
        "street": "100 Kent Street",
        "city": "Sydney",
        "state": "NSW",
        "postcode": "2000",
        "country": "AU",
        "latitude": -33.8688,
        "longitude": 151.2093
      },
      "retailer": { "id": "<retailerId>" },
      "openingSchedule": { "allHours": true, "monStart": 0, "monEnd": 0, "tueStart": 0, "tueEnd": 0, "wedStart": 0, "wedEnd": 0, "thuStart": 0, "thuEnd": 0, "friStart": 0, "friEnd": 0, "satStart": 0, "satEnd": 0, "sunStart": 0, "sunEnd": 0 }
    },
    {
      "ref": "LOC_STORE_01",
      "name": "CBD Store",
      "type": "STORE",
      "primaryAddress": {
        "ref": "LOC_STORE_01_ADDR",
        "name": "CBD Store",
        "street": "200 Bourke Street",
        "city": "Melbourne",
        "state": "VIC",
        "postcode": "3000",
        "country": "AU",
        "latitude": -37.8136,
        "longitude": 144.9631
      },
      "retailer": { "id": "<retailerId>" },
      "openingSchedule": { "allHours": false, "monStart": 540, "monEnd": 1080, "tueStart": 540, "tueEnd": 1080, "wedStart": 540, "wedEnd": 1080, "thuStart": 540, "thuEnd": 1080, "friStart": 540, "friEnd": 1080, "satStart": 600, "satEnd": 1020, "sunStart": 0, "sunEnd": 0 }
    }
  ],
  "returnFields": ["id", "ref", "name", "type", "status"]
}

**4. Create HD network + CC network:**

```json
{
  "mutation": "createNetwork",
  "inputs": [
    { "name": "NETWORK_HD", "type": "HD", "retailers": [{ "id": <retailerId> }] },
    { "name": "NETWORK_CC", "type": "CC", "retailers": [{ "id": <retailerId> }] }
  ],
  "returnFields": ["id", "ref", "type", "status"]
}
```

Then link locations to networks by updating the network with `locations` or creating a new network that includes location IDs. Use `graphql.introspect` with `type: "CreateNetworkInput"` to discover the exact linking mechanism.

**5. Create product catalogue:**

```graphql
mutation($input: CreateProductCatalogueInput!) {
  createProductCatalogue(input: $input) { id ref type status }
}
```
```json
{ "input": { "ref": "PC:MASTER:<RETAILER_REF>", "type": "MASTER", "name": "Master Catalogue", "retailerRefs": ["<RETAILER_REF>"] } }
```

**6. Create 3 sample products (1 standard + 2 variants):**

Standard product first:
```graphql
mutation($input: CreateStandardProductInput!) {
  createStandardProduct(input: $input) { id ref name status catalogue { ref } }
}
```
```json
{ "input": { "ref": "PROD_SAMPLE_001", "type": "STANDARD", "gtin": "PROD_SAMPLE_001", "name": "Sample Product", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } } }
```

Then variants via batch:
```json
{
  "mutation": "createVariantProduct",
  "inputs": [
    { "ref": "SKU_SAMPLE_001_S", "type": "VARIANT", "gtin": "SKUSAMPLE001S", "name": "Sample Product - Small", "product": { "ref": "PROD_SAMPLE_001", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } }, "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } },
    { "ref": "SKU_SAMPLE_001_M", "type": "VARIANT", "gtin": "SKUSAMPLE001M", "name": "Sample Product - Medium", "product": { "ref": "PROD_SAMPLE_001", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } }, "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } }
  ],
  "returnFields": ["id", "ref", "name", "status"]
}
```

**7. Create inventory positions (stock products at locations):**

Query inventory catalogue first: `{ inventoryCatalogues(first: 5) { edges { node { id ref type } } } }`. Use the catalogue ref (typically `DEFAULT:<retailerId>` or `COMPATIBILITY:<retailerId>`).

```json
{
  "mutation": "createInventoryPosition",
  "inputs": [
    { "ref": "SKU_SAMPLE_001_S", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_S", "locationRef": "LOC_WH_01", "onHand": 100 },
    { "ref": "SKU_SAMPLE_001_M", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_M", "locationRef": "LOC_WH_01", "onHand": 100 },
    { "ref": "SKU_SAMPLE_001_S", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_S", "locationRef": "LOC_STORE_01", "onHand": 25 },
    { "ref": "SKU_SAMPLE_001_M", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_M", "locationRef": "LOC_STORE_01", "onHand": 25 }
  ],
  "returnFields": ["id", "ref", "type", "status", "onHand"]
}
```

**8. Create virtual catalogue:**

```graphql
mutation($input: CreateVirtualCatalogueInput!) {
  createVirtualCatalogue(input: $input) { id ref type status }
}
```
```json
{ "input": { "ref": "VC:DEFAULT:<RETAILER_REF>", "type": "DEFAULT", "name": "Default Virtual Catalogue", "inventoryCatalogueRef": "DEFAULT:<retailerId>", "productCatalogueRef": "PC:MASTER:<RETAILER_REF>", "retailerRefs": ["<RETAILER_REF>"] } }
```

**9. Create 1 test customer:**

```graphql
mutation($input: CreateCustomerInput!) {
  createCustomer(input: $input) { id username firstName lastName primaryEmail }
}
```
```json
{ "input": { "username": "test-customer-001", "firstName": "Test", "lastName": "Customer", "primaryEmail": "test@example.com", "promotionOptIn": false, "retailer": { "id": "<retailerId>" } } }
```

**10. Verify everything is discoverable:**

Run `/fluent-test-data` discovery to confirm all entities are queryable and inventory is visible. The discovery sequence will query locations, networks, catalogues, products, inventory, and customers — confirming the full setup.

## Batch Setup for Scale

For production-like environments with many entities, use `batch.create` / `batch.send` instead of individual mutations.

### Bulk Locations (100+)

```
batch.create({ "name": "Bulk location load", "entityType": "LOCATION", "action": "UPSERT", "retailerId": "<retailerId>" })
```

Send records in batches of up to 500 per `batch.send` call. Each record follows the same shape as the `CreateLocationInput` variables.

### Bulk Products (1000+)

```
batch.create({ "name": "Bulk product load", "entityType": "STANDARD_PRODUCT", "action": "UPSERT", "retailerId": "<retailerId>" })
```

For variant products, use a separate batch job with `entityType: "VARIANT_PRODUCT"`. Standard products must be loaded first since variants depend on them.

### Bulk Inventory (10000+)

```
batch.create({ "name": "Bulk inventory load", "entityType": "INVENTORY_POSITION", "action": "UPSERT", "retailerId": "<retailerId>" })
```

Inventory batch payloads need `ref`, `productRef`, `locationRef`, and `onHand` at minimum.

### Batch Workflow

```
1. batch.create  -> get jobId
2. batch.send    -> send records (repeat for each page of data)
3. batch.status  -> poll until terminal (COMPLETE or FAILED)
4. batch.results -> inspect per-record success/failure
```

For multi-entity setups, create separate batch jobs per entity type and run them in dependency order (locations before networks, products before inventory).

## Integration with Other Skills

| Task | Skill |
|------|-------|
| Discover existing environment entities | `/fluent-test-data` |
| Create test orders and fulfilments | `/fluent-test-data` |
| Run E2E tests after setup | `/fluent-e2e-test` |
| Configure settings (consignment prefix, webhooks) | `/fluent-settings` |
| Build and deploy workflows | `/fluent-workflow-builder` |
| Trace event failures during setup | `/fluent-trace` |
| Analyze workflow prerequisites | `/fluent-workflow-analyzer` |
| MCP tool syntax reference | `/fluent-mcp-tools` |

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `createLocation` fails with schema error | Input fields differ by Fluent version | Use `graphql.introspect` with `type: "CreateLocationInput"` to discover exact fields |
| Network created but orders don't route | Locations not linked to network | Verify location-network assignment via location query with `networks` connection |
| Products created but not found in catalogue | Wrong `catalogue.ref` in product creation | Query `productCatalogues` first and use exact ref format |
| Inventory position created but `onHand` is 0 | `onHand` not set or quantity type mismatch | Verify `onHand` field in the mutation response; create explicit inventory quantities if needed |
| Virtual catalogue empty | Virtual positions not created | Create virtual positions linking products to the virtual catalogue |
| Duplicate ref error | Entity with same ref already exists | Query existing entities first or use `UPSERT` action in batch |
| Mutation rejected with permission error | User lacks retailer-level permissions | Check user roles via `connection.test` or query `me { roles }` |

## Tips

- **Always use `graphql.introspect` to discover exact input types** — field names and required fields vary by Fluent version and account configuration.
- **Use unique refs with timestamps for test environments** — Avoid collisions with existing data by appending `_<YYYYMMDD_HHmmss>` to refs.
- **Location addresses need latitude and longitude** — Distance-based routing will not work without coordinates. Use a geocoding service if needed.
- **Network-location assignment is critical** — Orders will not route to locations that are not assigned to the appropriate fulfilment network. Always verify after setup.
- **Inventory positions need both `ref` (product SKU) and `locationRef`** — The combination must be unique. Missing either field will cause the mutation to fail.
- **Use `batch.*` tools for anything over 50 entities** — Individual mutations are fine for small setups, but batch ingestion is significantly faster and more reliable at scale.
- **Virtual catalogues are optional but needed for fulfilment options and checkout** — If the retailer uses the fulfilment options API, virtual catalogues must exist.
- **Create entities in dependency order** — Follow the configuration sequence table strictly. Out-of-order creation will result in reference errors.
- **Schema varies by account** — Always use `graphql.introspect` if a mutation fails. Custom modules can add or modify input types.
- **Run `/fluent-test-data` discovery after setup** — This confirms all entities are queryable and validates the environment is workflow-ready.

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-retailer-config"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-retailer-config.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
