@fluentcommerce/ai-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/bin/cli.mjs +1973 -0
- package/content/cli/agents/fluent-cli/agent.json +149 -0
- package/content/cli/agents/fluent-cli.md +132 -0
- package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
- package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
- package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
- package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
- package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
- package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
- package/content/cli/skills/fluent-connect/SKILL.md +886 -0
- package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
- package/content/cli/skills/fluent-profile/SKILL.md +180 -0
- package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
- package/content/dev/agents/fluent-dev/agent.json +88 -0
- package/content/dev/agents/fluent-dev.md +525 -0
- package/content/dev/reference-modules/catalog.json +4754 -0
- package/content/dev/skills/fluent-build/SKILL.md +192 -0
- package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
- package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
- package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
- package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
- package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
- package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
- package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
- package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
- package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
- package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
- package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
- package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
- package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
- package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
- package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
- package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
- package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
- package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
- package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
- package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
- package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
- package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
- package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
- package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
- package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
- package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
- package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
- package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
- package/content/mcp-extn/agents/fluent-mcp.md +69 -0
- package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
- package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
- package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
- package/content/rfl/agents/fluent-rfl.md +56 -0
- package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
- package/docs/CAPABILITY_MAP.md +77 -0
- package/docs/CLI_COVERAGE.md +47 -0
- package/docs/DEV_WORKFLOW.md +802 -0
- package/docs/FLOW_RUN.md +142 -0
- package/docs/USE_CASES.md +404 -0
- package/metadata.json +156 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fluent-retailer-config
|
|
3
|
+
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".
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
|
6
|
+
argument-hint: [--retailer <ref>] [--setup-type full|locations|networks|catalogues|inventory]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Retailer Environment Configuration
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## Planning Gate
|
|
14
|
+
|
|
15
|
+
**Before creating any entities or modifying retailer configuration, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.**
|
|
16
|
+
|
|
17
|
+
**Retailer-config specific emphasis — ensure these are covered:**
|
|
18
|
+
|
|
19
|
+
1. **Business Context (Section 1)** — why this configuration is needed (Day-0 setup, new feature, environment cloning)
|
|
20
|
+
2. **Entity relationship diagram (Section 3)** — Mermaid diagram showing entities to create and their dependencies (locations, networks, catalogues, inventory). Validate syntax per `/fluent-mermaid-validate`
|
|
21
|
+
3. **Impacted retailers (Section 4.8)** — profile, retailer ref, retailer ID, environment URL
|
|
22
|
+
4. **GraphQL operations (Section 4.7)** — list of create/update mutations and batch operations to execute
|
|
23
|
+
5. **Detailed Design (Section 5)** — entities to create with refs, types, and key attributes (e.g., location: ref, name, type, openingSchedule)
|
|
24
|
+
6. **Deployment Sequence (Section 9)** — order of creation respecting dependencies (locations before networks, catalogues before inventory, products before positions)
|
|
25
|
+
7. **Rollback Plan (Section 10)** — note that entity creation is not easily reversible in Fluent; list what can be soft-deleted vs what persists
|
|
26
|
+
|
|
27
|
+
**Write the plan to:** `accounts/<PROFILE>/plans/<YYYY-MM-DD>-retailer-config-<slug>.md`. Set `Status: PENDING`.
|
|
28
|
+
|
|
29
|
+
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).
|
|
30
|
+
|
|
31
|
+
## Ownership Boundary
|
|
32
|
+
|
|
33
|
+
This skill owns retailer entity configuration: locations, networks, catalogues, products, inventory positions, virtual catalogues, carriers, and users.
|
|
34
|
+
|
|
35
|
+
Environment discovery (read-only queries) is owned by `/fluent-test-data`.
|
|
36
|
+
Settings management is owned by `/fluent-settings`.
|
|
37
|
+
Canonical MCP extension tool syntax/limits are owned by `/fluent-mcp-tools`.
|
|
38
|
+
|
|
39
|
+
## When to Use
|
|
40
|
+
|
|
41
|
+
- Setting up a brand new retailer from scratch
|
|
42
|
+
- Adding new locations (warehouses, stores, DCs) to an existing retailer
|
|
43
|
+
- Creating fulfilment networks and linking locations
|
|
44
|
+
- Setting up product catalogues and inventory for testing
|
|
45
|
+
- Cloning an environment's entity structure to another retailer
|
|
46
|
+
- Pre-go-live environment preparation
|
|
47
|
+
- Creating carriers for shipment tracking
|
|
48
|
+
- Provisioning users and assigning roles
|
|
49
|
+
|
|
50
|
+
## Configuration Sequence
|
|
51
|
+
|
|
52
|
+
Entities have dependencies. They MUST be created in this order:
|
|
53
|
+
|
|
54
|
+
| Step | Entity | Dependencies |
|
|
55
|
+
|------|--------|-------------|
|
|
56
|
+
| 1 | Retailer | None (usually already exists) |
|
|
57
|
+
| 2 | Locations (warehouses, stores, DCs) | Retailer |
|
|
58
|
+
| 3 | Networks | Retailer, Locations |
|
|
59
|
+
| 4 | Product Catalogues | Retailer |
|
|
60
|
+
| 5 | Products (Standard then Variant) | Product Catalogue |
|
|
61
|
+
| 6 | Inventory Positions + Quantities | Products, Locations |
|
|
62
|
+
| 7 | Virtual Catalogues + Virtual Positions | Products, Catalogues |
|
|
63
|
+
| 8 | Carriers | Retailer |
|
|
64
|
+
| 9 | Customers | Retailer |
|
|
65
|
+
| 10 | Users + Roles | Retailer |
|
|
66
|
+
|
|
67
|
+
**Do not skip steps.** Creating inventory before products exist will fail. Creating networks before locations exist means there is nothing to link.
|
|
68
|
+
|
|
69
|
+
### Schema Patterns (Global Inventory Model)
|
|
70
|
+
|
|
71
|
+
> **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.
|
|
72
|
+
|
|
73
|
+
| Entity | Scoping Field | Required Fields (non-obvious) |
|
|
74
|
+
|--------|--------------|-------------------------------|
|
|
75
|
+
| Location | `retailer: { id }` | `openingSchedule` (always required), `primaryAddress.ref`, `.latitude`, `.longitude` |
|
|
76
|
+
| Network | `retailers: [{ id }]` (array) | `name` (becomes ref), NO `ref` field |
|
|
77
|
+
| Product Catalogue | `retailerRefs: ["ref"]` (array of strings) | NO `retailer: { id }` |
|
|
78
|
+
| Standard Product | `catalogue: { ref }` | `type`, `gtin` (both required, gtin max 20 chars), NO `retailer`, NO `status` |
|
|
79
|
+
| Variant Product | `catalogue: { ref }` | `type`, `gtin` (max 20 chars), `product: { ref, catalogue: { ref } }` (compound key, NOT `standardProduct`), NO `retailer`, NO `status` |
|
|
80
|
+
| Inventory Position | `catalogue: { ref }` (InventoryCatalogueKey) | NO `retailer`, NO `status` |
|
|
81
|
+
| Virtual Catalogue | `retailerRefs: ["ref"]` (optional) | `inventoryCatalogueRef`, `productCatalogueRef` (both required), NO `retailer: { id }`, NO `status` |
|
|
82
|
+
| Virtual Position | `catalogue: { ref }` (VirtualCatalogueKey) | `quantity` (Int!, required), NO `retailer`, NO `status` |
|
|
83
|
+
| Carrier | `retailer: { id }` | NO `status` in create input |
|
|
84
|
+
| Customer | `retailer: { id }` | `promotionOptIn` (Boolean!, required), NO `ref` field, `username` is the identifier |
|
|
85
|
+
|
|
86
|
+
## Pre-flight Discovery Protocol
|
|
87
|
+
|
|
88
|
+
**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.
|
|
89
|
+
|
|
90
|
+
### Step 1: Identify the Account (from config, not GraphQL)
|
|
91
|
+
|
|
92
|
+
The Fluent **account name** is NOT available via GraphQL. Extract it from the base URL returned by `config.validate`:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Base URL pattern: https://<account>[.<environment>].api.fluentretail.com
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Base URL | Account | Environment |
|
|
99
|
+
|----------|---------|-------------|
|
|
100
|
+
| `https://example-account.test.api.fluentretail.com` | `example-account` | **test** |
|
|
101
|
+
| `https://example-account.sandbox.api.fluentretail.com` | `example-account` | **sandbox** |
|
|
102
|
+
| `https://example-account.api.fluentretail.com` | `example-account` | **production** (no qualifier) |
|
|
103
|
+
| `https://another-account.sandbox.api.fluentretail.com` | `another-account` | **sandbox** |
|
|
104
|
+
|
|
105
|
+
**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.
|
|
106
|
+
|
|
107
|
+
### Step 2: Discovery Query (run once, cache results)
|
|
108
|
+
|
|
109
|
+
```graphql
|
|
110
|
+
{
|
|
111
|
+
me { id username primaryRetailer { id ref tradingName status } }
|
|
112
|
+
locations(first: 5) { edges { node { id ref name type status } } pageInfo { hasNextPage } }
|
|
113
|
+
networks(first: 10) { edges { node { id ref type status } } }
|
|
114
|
+
productCatalogues(first: 5) { edges { node { id ref type status } } }
|
|
115
|
+
inventoryCatalogues(first: 5) { edges { node { id ref type status } } }
|
|
116
|
+
virtualCatalogues(first: 5) { edges { node { id ref type status } } }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> **NOTE:** The User type has `primaryRetailer`, NOT `retailer`. Using `me { retailer { ... } }` will fail with "Field 'retailer' in type 'User' is undefined".
|
|
121
|
+
|
|
122
|
+
### Required Context Variables
|
|
123
|
+
|
|
124
|
+
From the discovery response and config, extract and store these variables. **All subsequent mutations reference them.**
|
|
125
|
+
|
|
126
|
+
| Variable | Source | Example | Used By |
|
|
127
|
+
|----------|--------|---------|---------|
|
|
128
|
+
| `accountName` | `config.validate` → baseUrl first segment | `"example-account"` | Logging, rule name prefixes |
|
|
129
|
+
| `environment` | `config.validate` → baseUrl qualifier | `"test"` / `"sandbox"` / `"production"` | Safety checks |
|
|
130
|
+
| `retailerId` | `me.primaryRetailer.id` | `"2"` | Location, Network, Carrier, Customer |
|
|
131
|
+
| `retailerRef` | `me.primaryRetailer.ref` | `"RETAILER_REF"` | Product Catalogue (`retailerRefs`), Virtual Catalogue |
|
|
132
|
+
| `productCatalogueRef` | `productCatalogues.edges[0].node.ref` | `"PC:MASTER:2"` | Products (`catalogue`), Virtual Catalogue |
|
|
133
|
+
| `inventoryCatalogueRef` | `inventoryCatalogues.edges[0].node.ref` | `"DEFAULT:2"` | Inventory Positions (`catalogue`), Virtual Catalogue |
|
|
134
|
+
| `existingLocations` | `locations.edges[*].node` | array | Network creation (location IDs) |
|
|
135
|
+
| `existingNetworks` | `networks.edges[*].node` | array | Avoid duplicates |
|
|
136
|
+
|
|
137
|
+
### Validation Checklist (before first mutation)
|
|
138
|
+
|
|
139
|
+
- [ ] `config.validate()` returns `ok: true` — capture `baseUrl` for account/environment
|
|
140
|
+
- [ ] `connection.test()` returns `ok: true`
|
|
141
|
+
- [ ] `environment` identified — if **production**, require explicit user confirmation before writes
|
|
142
|
+
- [ ] `retailerId` is captured (Int, used as string in `retailer: { id }`)
|
|
143
|
+
- [ ] `retailerRef` is captured (used in `retailerRefs` arrays)
|
|
144
|
+
- [ ] `productCatalogueRef` exists — if empty, create one first (Step 4)
|
|
145
|
+
- [ ] `inventoryCatalogueRef` exists — if empty, account may need Global Inventory enabled
|
|
146
|
+
- [ ] No duplicate refs — check `existingLocations` before creating new ones
|
|
147
|
+
|
|
148
|
+
## Input Validation Rules
|
|
149
|
+
|
|
150
|
+
**Run these checks on EVERY mutation input before sending.** These constraints are enforced by the GraphQL schema and produce cryptic errors if violated.
|
|
151
|
+
|
|
152
|
+
### Per-Entity Validation
|
|
153
|
+
|
|
154
|
+
**Location:**
|
|
155
|
+
- `openingSchedule` MUST be present (even for 24/7, use `allHours: true` with all zeros)
|
|
156
|
+
- `primaryAddress.ref` MUST be present and unique
|
|
157
|
+
- `primaryAddress.latitude` and `.longitude` MUST be present (Float, not String)
|
|
158
|
+
- `retailer.id` must be a String: `{ "id": "2" }` not `{ "id": 2 }`
|
|
159
|
+
|
|
160
|
+
**Network:**
|
|
161
|
+
- Use `name` — there is NO `ref` field. The `name` value becomes the `ref` in the response
|
|
162
|
+
- `retailers` is an array: `[{ "id": 2 }]` (note: Int here, not String)
|
|
163
|
+
- `locations` is optional but recommended at creation: `[{ "id": 134 }]` (Int)
|
|
164
|
+
|
|
165
|
+
**Products (Standard + Variant):**
|
|
166
|
+
- `gtin` max **20 characters** — if product ref > 20 chars, strip underscores or generate shorter code
|
|
167
|
+
- `type` is required (e.g., `"STANDARD"`, `"VARIANT"`)
|
|
168
|
+
- NO `retailer` field — scope via `catalogue: { ref: "<productCatalogueRef>" }`
|
|
169
|
+
- NO `status` field — auto-set to `CREATED`
|
|
170
|
+
- Variant `product` is a **compound key**: `{ "ref": "<parentRef>", "catalogue": { "ref": "<productCatalogueRef>" } }`
|
|
171
|
+
- Variant `product` field name is `product`, NOT `standardProduct`
|
|
172
|
+
|
|
173
|
+
**Inventory Position:**
|
|
174
|
+
- `catalogue` uses `InventoryCatalogueKey`: `{ "ref": "<inventoryCatalogueRef>" }`
|
|
175
|
+
- NO `retailer` field, NO `status` field
|
|
176
|
+
- `ref` + `locationRef` combination must be unique within the catalogue
|
|
177
|
+
|
|
178
|
+
**Virtual Catalogue:**
|
|
179
|
+
- `inventoryCatalogueRef` (String!) and `productCatalogueRef` (String!) are BOTH required
|
|
180
|
+
- Uses `retailerRefs: ["<retailerRef>"]` (array of strings), NOT `retailer: { id }`
|
|
181
|
+
- NO `status` field
|
|
182
|
+
|
|
183
|
+
**Virtual Position:**
|
|
184
|
+
- `quantity` (Int!) is required — use `0` on creation
|
|
185
|
+
- `catalogue` uses `VirtualCatalogueKey`: `{ "ref": "<virtualCatalogueRef>" }`
|
|
186
|
+
|
|
187
|
+
**Customer:**
|
|
188
|
+
- NO `ref` field — `username` is the identifier
|
|
189
|
+
- `promotionOptIn` (Boolean!) is required — use `false` for test customers
|
|
190
|
+
- `firstName` and `lastName` are optional (despite appearing essential)
|
|
191
|
+
|
|
192
|
+
**Carrier:**
|
|
193
|
+
- NO `status` field — auto-set to `CREATED`
|
|
194
|
+
|
|
195
|
+
### Auto-Fix Patterns
|
|
196
|
+
|
|
197
|
+
When building mutation inputs, apply these transformations automatically:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
gtin_value = product_ref.replace("_", "").slice(0, 20)
|
|
201
|
+
if len(gtin_value) > 20: gtin_value = gtin_value[:20]
|
|
202
|
+
|
|
203
|
+
variant_product_link = {
|
|
204
|
+
"ref": parent_product_ref,
|
|
205
|
+
"catalogue": { "ref": discovered.productCatalogueRef }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
inventory_catalogue_key = { "ref": discovered.inventoryCatalogueRef }
|
|
209
|
+
|
|
210
|
+
opening_schedule_24x7 = {
|
|
211
|
+
"allHours": true,
|
|
212
|
+
"monStart": 0, "monEnd": 0, "tueStart": 0, "tueEnd": 0,
|
|
213
|
+
"wedStart": 0, "wedEnd": 0, "thuStart": 0, "thuEnd": 0,
|
|
214
|
+
"friStart": 0, "friEnd": 0, "satStart": 0, "satEnd": 0,
|
|
215
|
+
"sunStart": 0, "sunEnd": 0
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Ref Collision Prevention
|
|
220
|
+
|
|
221
|
+
Before creating any entity, query for existing refs to avoid duplicate errors:
|
|
222
|
+
|
|
223
|
+
```graphql
|
|
224
|
+
{ locations(first: 1, ref: ["<proposed_ref>"]) { edges { node { id ref } } } }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
If edges is non-empty, the ref already exists. Either:
|
|
228
|
+
1. Append a timestamp suffix: `<ref>_<YYYYMMDD_HHmmss>`
|
|
229
|
+
2. Use the existing entity's ID for subsequent operations
|
|
230
|
+
|
|
231
|
+
## Step 1: Verify Connectivity and Retailer
|
|
232
|
+
|
|
233
|
+
Run the Pre-flight Discovery Protocol above. If you have already run discovery, skip to Step 2.
|
|
234
|
+
|
|
235
|
+
**Quick connectivity check:**
|
|
236
|
+
```
|
|
237
|
+
connection.test()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Full discovery (captures all context variables):**
|
|
241
|
+
|
|
242
|
+
Use the discovery query from the Pre-flight section. Capture `retailerId`, `retailerRef`, `productCatalogueRef`, `inventoryCatalogueRef`, and existing entity lists.
|
|
243
|
+
|
|
244
|
+
## Step 2: Location Management
|
|
245
|
+
|
|
246
|
+
Locations are physical or logical sites where inventory is held and orders are fulfilled. Every fulfilment workflow depends on locations existing first.
|
|
247
|
+
|
|
248
|
+
### Location Types
|
|
249
|
+
|
|
250
|
+
| Type | Purpose | Typical Use |
|
|
251
|
+
|------|---------|-------------|
|
|
252
|
+
| `WAREHOUSE` | Centralized distribution centre | HD (home delivery) sourcing |
|
|
253
|
+
| `STORE` | Retail store | CC (click & collect), SFS (ship from store) |
|
|
254
|
+
| `DC` | Distribution centre (alias) | Same as WAREHOUSE in some configurations |
|
|
255
|
+
|
|
256
|
+
### Create Location (Warehouse)
|
|
257
|
+
|
|
258
|
+
```graphql
|
|
259
|
+
mutation($input: CreateLocationInput!) {
|
|
260
|
+
createLocation(input: $input) {
|
|
261
|
+
id
|
|
262
|
+
ref
|
|
263
|
+
name
|
|
264
|
+
type
|
|
265
|
+
status
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Variables:
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"input": {
|
|
274
|
+
"ref": "LOC_WH_SYDNEY_01",
|
|
275
|
+
"name": "Sydney Warehouse",
|
|
276
|
+
"type": "WAREHOUSE",
|
|
277
|
+
"supportPhoneNumber": "+61200000001",
|
|
278
|
+
"primaryAddress": {
|
|
279
|
+
"ref": "LOC_WH_SYDNEY_01_ADDR",
|
|
280
|
+
"name": "Sydney Warehouse",
|
|
281
|
+
"street": "100 Kent Street",
|
|
282
|
+
"city": "Sydney",
|
|
283
|
+
"state": "NSW",
|
|
284
|
+
"postcode": "2000",
|
|
285
|
+
"country": "AU",
|
|
286
|
+
"latitude": -33.8688,
|
|
287
|
+
"longitude": 151.2093
|
|
288
|
+
},
|
|
289
|
+
"retailer": { "id": "<retailerId>" },
|
|
290
|
+
"openingSchedule": {
|
|
291
|
+
"allHours": true,
|
|
292
|
+
"monStart": 0, "monEnd": 0,
|
|
293
|
+
"tueStart": 0, "tueEnd": 0,
|
|
294
|
+
"wedStart": 0, "wedEnd": 0,
|
|
295
|
+
"thuStart": 0, "thuEnd": 0,
|
|
296
|
+
"friStart": 0, "friEnd": 0,
|
|
297
|
+
"satStart": 0, "satEnd": 0,
|
|
298
|
+
"sunStart": 0, "sunEnd": 0
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**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).
|
|
305
|
+
|
|
306
|
+
### Create Location (Store)
|
|
307
|
+
|
|
308
|
+
Same mutation, different type and schedule:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"input": {
|
|
313
|
+
"ref": "LOC_STORE_MELB_01",
|
|
314
|
+
"name": "Melbourne CBD Store",
|
|
315
|
+
"type": "STORE",
|
|
316
|
+
"supportPhoneNumber": "+61300000001",
|
|
317
|
+
"primaryAddress": {
|
|
318
|
+
"ref": "LOC_STORE_MELB_01_ADDR",
|
|
319
|
+
"name": "Melbourne CBD Store",
|
|
320
|
+
"street": "200 Bourke Street",
|
|
321
|
+
"city": "Melbourne",
|
|
322
|
+
"state": "VIC",
|
|
323
|
+
"postcode": "3000",
|
|
324
|
+
"country": "AU",
|
|
325
|
+
"latitude": -37.8136,
|
|
326
|
+
"longitude": 144.9631
|
|
327
|
+
},
|
|
328
|
+
"retailer": { "id": "<retailerId>" },
|
|
329
|
+
"openingSchedule": {
|
|
330
|
+
"allHours": false,
|
|
331
|
+
"monStart": 540, "monEnd": 1080,
|
|
332
|
+
"tueStart": 540, "tueEnd": 1080,
|
|
333
|
+
"wedStart": 540, "wedEnd": 1080,
|
|
334
|
+
"thuStart": 540, "thuEnd": 1080,
|
|
335
|
+
"friStart": 540, "friEnd": 1080,
|
|
336
|
+
"satStart": 600, "satEnd": 1020,
|
|
337
|
+
"sunStart": 0, "sunEnd": 0
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Update Location
|
|
344
|
+
|
|
345
|
+
```graphql
|
|
346
|
+
mutation($input: UpdateLocationInput!) {
|
|
347
|
+
updateLocation(input: $input) {
|
|
348
|
+
id
|
|
349
|
+
ref
|
|
350
|
+
name
|
|
351
|
+
type
|
|
352
|
+
status
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Variables:
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"input": {
|
|
361
|
+
"id": "<location_id>",
|
|
362
|
+
"status": "INACTIVE"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Location Notes
|
|
368
|
+
|
|
369
|
+
- **Address is critical** — latitude and longitude are required for distance-based fulfilment routing. Orders will not route correctly to locations without coordinates.
|
|
370
|
+
- **Status lifecycle:** `ACTIVE` (available for fulfilment) or `INACTIVE` (excluded from routing).
|
|
371
|
+
- **Refs must be unique** across the retailer. Use a consistent naming convention like `LOC_<TYPE>_<CITY>_<SEQ>`.
|
|
372
|
+
- **Use `graphql.introspect` with `type: "CreateLocationInput"`** to discover exact fields for your Fluent version.
|
|
373
|
+
|
|
374
|
+
## Step 3: Network Management
|
|
375
|
+
|
|
376
|
+
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.
|
|
377
|
+
|
|
378
|
+
### Create Network
|
|
379
|
+
|
|
380
|
+
```graphql
|
|
381
|
+
mutation($input: CreateNetworkInput!) {
|
|
382
|
+
createNetwork(input: $input) {
|
|
383
|
+
id
|
|
384
|
+
ref
|
|
385
|
+
type
|
|
386
|
+
status
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Variables:
|
|
392
|
+
```json
|
|
393
|
+
{
|
|
394
|
+
"input": {
|
|
395
|
+
"name": "NETWORK_HD",
|
|
396
|
+
"type": "HD",
|
|
397
|
+
"retailers": [{ "id": <retailerId> }],
|
|
398
|
+
"locations": [{ "id": <warehouseLocationId> }]
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**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.
|
|
404
|
+
|
|
405
|
+
For click & collect:
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"input": {
|
|
409
|
+
"name": "NETWORK_CC",
|
|
410
|
+
"type": "CC",
|
|
411
|
+
"retailers": [{ "id": <retailerId> }],
|
|
412
|
+
"locations": [{ "id": <storeLocationId> }]
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Link Locations to Networks
|
|
418
|
+
|
|
419
|
+
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.
|
|
420
|
+
|
|
421
|
+
**Option A — Update location with network assignment:**
|
|
422
|
+
|
|
423
|
+
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:
|
|
424
|
+
|
|
425
|
+
```graphql
|
|
426
|
+
mutation($input: UpdateLocationInput!) {
|
|
427
|
+
updateLocation(input: $input) {
|
|
428
|
+
id
|
|
429
|
+
ref
|
|
430
|
+
networks {
|
|
431
|
+
edges {
|
|
432
|
+
node { id ref }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Option B — Batch ingestion for bulk assignment:**
|
|
440
|
+
|
|
441
|
+
For assigning many locations to a network at once, use the batch ingestion tools:
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
batch.create({
|
|
445
|
+
"name": "Link locations to HD network",
|
|
446
|
+
"entityType": "LOCATION",
|
|
447
|
+
"action": "UPSERT"
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Then send records with network references via `batch.send`.
|
|
452
|
+
|
|
453
|
+
**Option C — Attributes-based linkage:**
|
|
454
|
+
|
|
455
|
+
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.
|
|
456
|
+
|
|
457
|
+
### Network Notes
|
|
458
|
+
|
|
459
|
+
- **One network per fulfilment type** is the standard pattern (one HD, one CC, one SFS).
|
|
460
|
+
- **Network-location linkage is mandatory** — orders will not route to locations that are not assigned to the appropriate network.
|
|
461
|
+
- **Always verify linkage** after creation by querying the location's networks connection.
|
|
462
|
+
|
|
463
|
+
## Step 4: Catalogue Management
|
|
464
|
+
|
|
465
|
+
Product catalogues are containers for products. You need at least one catalogue before you can create any products.
|
|
466
|
+
|
|
467
|
+
### Create Product Catalogue
|
|
468
|
+
|
|
469
|
+
```graphql
|
|
470
|
+
mutation($input: CreateProductCatalogueInput!) {
|
|
471
|
+
createProductCatalogue(input: $input) {
|
|
472
|
+
id
|
|
473
|
+
ref
|
|
474
|
+
type
|
|
475
|
+
status
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Variables:
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"input": {
|
|
484
|
+
"ref": "PC:MASTER:<RETAILER_REF>",
|
|
485
|
+
"type": "MASTER",
|
|
486
|
+
"name": "Master Product Catalogue",
|
|
487
|
+
"retailerRefs": ["<RETAILER_REF>"]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**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.
|
|
493
|
+
|
|
494
|
+
### Catalogue Notes
|
|
495
|
+
|
|
496
|
+
- **Ref format convention:** `PC:MASTER:<retailer_ref>` or `PC:<TYPE>:<retailer_ref>`. Query existing catalogues first to match the naming convention.
|
|
497
|
+
- **Types:** `MASTER` is the primary catalogue. Some accounts use additional types like `SEASONAL` or `MARKDOWN`.
|
|
498
|
+
- **One master catalogue per retailer** is the standard pattern.
|
|
499
|
+
|
|
500
|
+
## Step 5: Product Management
|
|
501
|
+
|
|
502
|
+
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").
|
|
503
|
+
|
|
504
|
+
### Create Standard Product
|
|
505
|
+
|
|
506
|
+
```graphql
|
|
507
|
+
mutation($input: CreateStandardProductInput!) {
|
|
508
|
+
createStandardProduct(input: $input) {
|
|
509
|
+
id
|
|
510
|
+
ref
|
|
511
|
+
name
|
|
512
|
+
status
|
|
513
|
+
catalogue { ref }
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Variables:
|
|
519
|
+
```json
|
|
520
|
+
{
|
|
521
|
+
"input": {
|
|
522
|
+
"ref": "PROD_TSHIRT_001",
|
|
523
|
+
"type": "STANDARD",
|
|
524
|
+
"gtin": "PROD_TSHIRT_001",
|
|
525
|
+
"name": "Classic T-Shirt",
|
|
526
|
+
"catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" }
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Create Variant Product
|
|
532
|
+
|
|
533
|
+
```graphql
|
|
534
|
+
mutation($input: CreateVariantProductInput!) {
|
|
535
|
+
createVariantProduct(input: $input) {
|
|
536
|
+
id
|
|
537
|
+
ref
|
|
538
|
+
name
|
|
539
|
+
status
|
|
540
|
+
catalogue { ref }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Variables:
|
|
546
|
+
```json
|
|
547
|
+
{
|
|
548
|
+
"input": {
|
|
549
|
+
"ref": "SKU_TSHIRT_001_M_BLU",
|
|
550
|
+
"type": "VARIANT",
|
|
551
|
+
"gtin": "SKU_TSHIRT_001_M_BLU",
|
|
552
|
+
"name": "Classic T-Shirt - Medium Blue",
|
|
553
|
+
"product": { "ref": "PROD_TSHIRT_001", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } },
|
|
554
|
+
"catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" }
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Product Notes
|
|
560
|
+
|
|
561
|
+
- **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.
|
|
562
|
+
- **`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).
|
|
563
|
+
- **`status` is auto-set to `CREATED`** on creation — there is no `status` field in the create input. Status advances via workflow events.
|
|
564
|
+
- **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.
|
|
565
|
+
- **Standard Products are parents, Variant Products are children.** You must create the Standard Product before its Variants.
|
|
566
|
+
- **Product ref = SKU ref** used in order items. The variant product ref is what appears in `productRef` on order line items.
|
|
567
|
+
- **Use `graphql.batchMutate`** for creating multiple products:
|
|
568
|
+
```json
|
|
569
|
+
{
|
|
570
|
+
"mutation": "createVariantProduct",
|
|
571
|
+
"inputs": [
|
|
572
|
+
{ "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" } },
|
|
573
|
+
{ "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" } }
|
|
574
|
+
],
|
|
575
|
+
"returnFields": ["id", "ref", "name", "status"]
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Step 6: Inventory Management
|
|
580
|
+
|
|
581
|
+
Inventory positions link products to locations with stock quantities. Without inventory, products cannot be allocated to fulfilments.
|
|
582
|
+
|
|
583
|
+
### Create Inventory Position
|
|
584
|
+
|
|
585
|
+
```graphql
|
|
586
|
+
mutation($input: CreateInventoryPositionInput!) {
|
|
587
|
+
createInventoryPosition(input: $input) {
|
|
588
|
+
id
|
|
589
|
+
ref
|
|
590
|
+
type
|
|
591
|
+
status
|
|
592
|
+
onHand
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Variables:
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"input": {
|
|
601
|
+
"ref": "SKU_TSHIRT_001_M_BLU",
|
|
602
|
+
"type": "DEFAULT",
|
|
603
|
+
"catalogue": { "ref": "DEFAULT:<retailerId>" },
|
|
604
|
+
"productRef": "SKU_TSHIRT_001_M_BLU",
|
|
605
|
+
"locationRef": "LOC_WH_SYDNEY_01",
|
|
606
|
+
"onHand": 100
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**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 } } } }`.
|
|
612
|
+
|
|
613
|
+
### Create Inventory Quantity
|
|
614
|
+
|
|
615
|
+
For more granular inventory control (e.g., setting specific quantity types like SALE, RESERVED):
|
|
616
|
+
|
|
617
|
+
```graphql
|
|
618
|
+
mutation($input: CreateInventoryQuantityInput!) {
|
|
619
|
+
createInventoryQuantity(input: $input) {
|
|
620
|
+
id
|
|
621
|
+
ref
|
|
622
|
+
type
|
|
623
|
+
quantity {
|
|
624
|
+
... on Count { value }
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Variables:
|
|
631
|
+
```json
|
|
632
|
+
{
|
|
633
|
+
"input": {
|
|
634
|
+
"ref": "SKU_TSHIRT_001_M_BLU_QTY_SALE",
|
|
635
|
+
"type": "SALE",
|
|
636
|
+
"quantity": 100,
|
|
637
|
+
"inventoryPosition": { "ref": "SKU_TSHIRT_001_M_BLU" },
|
|
638
|
+
"catalogue": { "ref": "DEFAULT:<retailerId>" }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Bulk Inventory via Batch Ingestion
|
|
644
|
+
|
|
645
|
+
For large inventory datasets (100+ positions), use the batch tools:
|
|
646
|
+
|
|
647
|
+
**Step A — Create batch job:**
|
|
648
|
+
```
|
|
649
|
+
batch.create({
|
|
650
|
+
"name": "Inventory load - <TIMESTAMP>",
|
|
651
|
+
"entityType": "INVENTORY_POSITION",
|
|
652
|
+
"action": "UPSERT",
|
|
653
|
+
"retailerId": "<retailerId>"
|
|
654
|
+
})
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Step B — Send records:**
|
|
658
|
+
```
|
|
659
|
+
batch.send({
|
|
660
|
+
"jobId": "<job_id_from_step_A>",
|
|
661
|
+
"payload": {
|
|
662
|
+
"items": [
|
|
663
|
+
{
|
|
664
|
+
"ref": "SKU_001_S_RED",
|
|
665
|
+
"type": "DEFAULT",
|
|
666
|
+
"status": "ACTIVE",
|
|
667
|
+
"productRef": "SKU_001_S_RED",
|
|
668
|
+
"locationRef": "LOC_WH_SYDNEY_01",
|
|
669
|
+
"onHand": 50
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"ref": "SKU_001_M_RED",
|
|
673
|
+
"type": "DEFAULT",
|
|
674
|
+
"status": "ACTIVE",
|
|
675
|
+
"productRef": "SKU_001_M_RED",
|
|
676
|
+
"locationRef": "LOC_WH_SYDNEY_01",
|
|
677
|
+
"onHand": 75
|
|
678
|
+
}
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**Step C — Poll status:**
|
|
685
|
+
```
|
|
686
|
+
batch.status({ "jobId": "<job_id>" })
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
Repeat until terminal status (COMPLETE or FAILED).
|
|
690
|
+
|
|
691
|
+
**Step D — Check results:**
|
|
692
|
+
```
|
|
693
|
+
batch.results({ "jobId": "<job_id>" })
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Inventory Notes
|
|
697
|
+
|
|
698
|
+
- **Inventory position ref typically matches the product SKU ref.** The combination of `ref` + `locationRef` must be unique.
|
|
699
|
+
- **`onHand`** is the primary stock count used by allocation rules. Products with `onHand: 0` will not be allocated.
|
|
700
|
+
- **Use `batch.*` tools for anything over 50 entities.** Batch ingestion is significantly faster and more reliable for large datasets.
|
|
701
|
+
|
|
702
|
+
## Step 7: Virtual Catalogue Setup
|
|
703
|
+
|
|
704
|
+
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.
|
|
705
|
+
|
|
706
|
+
### Create Virtual Catalogue
|
|
707
|
+
|
|
708
|
+
```graphql
|
|
709
|
+
mutation($input: CreateVirtualCatalogueInput!) {
|
|
710
|
+
createVirtualCatalogue(input: $input) {
|
|
711
|
+
id
|
|
712
|
+
ref
|
|
713
|
+
type
|
|
714
|
+
status
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Variables:
|
|
720
|
+
```json
|
|
721
|
+
{
|
|
722
|
+
"input": {
|
|
723
|
+
"ref": "VC:DEFAULT:<RETAILER_REF>",
|
|
724
|
+
"type": "DEFAULT",
|
|
725
|
+
"name": "Default Virtual Catalogue",
|
|
726
|
+
"inventoryCatalogueRef": "DEFAULT:<retailerId>",
|
|
727
|
+
"productCatalogueRef": "PC:MASTER:<RETAILER_REF>",
|
|
728
|
+
"retailerRefs": ["<RETAILER_REF>"]
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**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`.
|
|
734
|
+
|
|
735
|
+
### Create Virtual Position
|
|
736
|
+
|
|
737
|
+
Virtual positions link virtual catalogues to product inventory:
|
|
738
|
+
|
|
739
|
+
```graphql
|
|
740
|
+
mutation($input: CreateVirtualPositionInput!) {
|
|
741
|
+
createVirtualPosition(input: $input) {
|
|
742
|
+
id
|
|
743
|
+
ref
|
|
744
|
+
type
|
|
745
|
+
status
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Variables:
|
|
751
|
+
```json
|
|
752
|
+
{
|
|
753
|
+
"input": {
|
|
754
|
+
"ref": "VP:SKU_TSHIRT_001_M_BLU",
|
|
755
|
+
"type": "DEFAULT",
|
|
756
|
+
"productRef": "SKU_TSHIRT_001_M_BLU",
|
|
757
|
+
"quantity": 0,
|
|
758
|
+
"catalogue": { "ref": "VC:DEFAULT:<RETAILER_REF>" }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Virtual Catalogue Notes
|
|
764
|
+
|
|
765
|
+
- **Virtual catalogues require both `inventoryCatalogueRef` and `productCatalogueRef`** — these link the virtual catalogue to the underlying inventory and product catalogues.
|
|
766
|
+
- **Virtual positions require `quantity` (Int!)** — set to `0` on creation; the workflow/orchestration recalculates the aggregated quantity from underlying inventory.
|
|
767
|
+
- **No `retailer: { id }` on virtual entities** — use `retailerRefs` (array of ref strings) on the catalogue, and `catalogue: { ref }` on positions.
|
|
768
|
+
- **No `status` field in create inputs** — status defaults to `CREATED` and advances via workflow events.
|
|
769
|
+
- **Virtual catalogues are optional** but needed for fulfilment options and checkout APIs.
|
|
770
|
+
- **Ref format convention:** `VC:<TYPE>:<retailer_ref>` for catalogues, `VP:<product_ref>` for positions.
|
|
771
|
+
|
|
772
|
+
## Step 8: Carrier Setup
|
|
773
|
+
|
|
774
|
+
Carriers represent shipping providers. They are required when shipment tracking is part of the fulfilment workflow.
|
|
775
|
+
|
|
776
|
+
### Create Carrier
|
|
777
|
+
|
|
778
|
+
```graphql
|
|
779
|
+
mutation($input: CreateCarrierInput!) {
|
|
780
|
+
createCarrier(input: $input) {
|
|
781
|
+
id
|
|
782
|
+
ref
|
|
783
|
+
name
|
|
784
|
+
status
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Variables:
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"input": {
|
|
793
|
+
"ref": "CARRIER_DHL",
|
|
794
|
+
"name": "DHL Express",
|
|
795
|
+
"type": "SHIPPING",
|
|
796
|
+
"retailer": { "id": "<retailerId>" }
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**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.
|
|
802
|
+
|
|
803
|
+
## Step 9: Customer Setup
|
|
804
|
+
|
|
805
|
+
Customer creation is already covered in detail by `/fluent-test-data`. Use that skill for creating test customers. The core mutation for reference:
|
|
806
|
+
|
|
807
|
+
```graphql
|
|
808
|
+
mutation($input: CreateCustomerInput!) {
|
|
809
|
+
createCustomer(input: $input) {
|
|
810
|
+
id
|
|
811
|
+
username
|
|
812
|
+
firstName
|
|
813
|
+
lastName
|
|
814
|
+
primaryEmail
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Variables:
|
|
820
|
+
```json
|
|
821
|
+
{
|
|
822
|
+
"input": {
|
|
823
|
+
"username": "test-customer-<TIMESTAMP>",
|
|
824
|
+
"firstName": "Test",
|
|
825
|
+
"lastName": "Customer",
|
|
826
|
+
"primaryEmail": "test.customer@example.com",
|
|
827
|
+
"promotionOptIn": false,
|
|
828
|
+
"retailer": { "id": "<retailerId>" }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
## Step 10: User and Role Management
|
|
834
|
+
|
|
835
|
+
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.
|
|
836
|
+
|
|
837
|
+
### Create User
|
|
838
|
+
|
|
839
|
+
Use `graphql.introspect` with `mutation: "createUser"` to discover the exact input type. Common pattern:
|
|
840
|
+
|
|
841
|
+
```graphql
|
|
842
|
+
mutation($input: CreateUserInput!) {
|
|
843
|
+
createUser(input: $input) {
|
|
844
|
+
id
|
|
845
|
+
ref
|
|
846
|
+
username
|
|
847
|
+
status
|
|
848
|
+
primaryEmail
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Common Roles
|
|
854
|
+
|
|
855
|
+
| Role | Purpose |
|
|
856
|
+
|------|---------|
|
|
857
|
+
| `RETAILER_ADMIN` | Full retailer access |
|
|
858
|
+
| `LOCATION_USER` | Location-scoped operations (store staff) |
|
|
859
|
+
| `FLUENT_ADMIN` | Platform-level admin (account-scoped) |
|
|
860
|
+
|
|
861
|
+
### User Management Notes
|
|
862
|
+
|
|
863
|
+
- **Fluent CLI is often simpler** for user management: `fluent profile create` handles user creation and authentication setup.
|
|
864
|
+
- **Roles are account-specific.** Use `graphql.introspect` to discover available role types for your account.
|
|
865
|
+
- **Location-scoped users** need both a role assignment and a location assignment to function correctly.
|
|
866
|
+
|
|
867
|
+
## Full Setup Recipe
|
|
868
|
+
|
|
869
|
+
A step-by-step recipe for setting up a complete retailer environment from scratch. All 10 steps in one sequence.
|
|
870
|
+
|
|
871
|
+
### Prerequisites
|
|
872
|
+
|
|
873
|
+
- Fluent MCP extension connected (`connection.test` returns success)
|
|
874
|
+
- Retailer ID known (query via `graphql.query`)
|
|
875
|
+
|
|
876
|
+
### Step-by-Step
|
|
877
|
+
|
|
878
|
+
**1. Verify connectivity:**
|
|
879
|
+
```
|
|
880
|
+
connection.test()
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
**2. Check retailer exists:**
|
|
884
|
+
```graphql
|
|
885
|
+
{
|
|
886
|
+
retailers(first: 10) {
|
|
887
|
+
edges {
|
|
888
|
+
node { id ref tradingName status }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
Capture `retailerId` from the response.
|
|
895
|
+
|
|
896
|
+
**3. Create 1 warehouse + 1 store:**
|
|
897
|
+
|
|
898
|
+
Use `graphql.batchMutate` to create both in one call:
|
|
899
|
+
```json
|
|
900
|
+
{
|
|
901
|
+
"mutation": "createLocation",
|
|
902
|
+
"inputs": [
|
|
903
|
+
{
|
|
904
|
+
"ref": "LOC_WH_01",
|
|
905
|
+
"name": "Primary Warehouse",
|
|
906
|
+
"type": "WAREHOUSE",
|
|
907
|
+
"primaryAddress": {
|
|
908
|
+
"ref": "LOC_WH_01_ADDR",
|
|
909
|
+
"name": "Primary Warehouse",
|
|
910
|
+
"street": "100 Kent Street",
|
|
911
|
+
"city": "Sydney",
|
|
912
|
+
"state": "NSW",
|
|
913
|
+
"postcode": "2000",
|
|
914
|
+
"country": "AU",
|
|
915
|
+
"latitude": -33.8688,
|
|
916
|
+
"longitude": 151.2093
|
|
917
|
+
},
|
|
918
|
+
"retailer": { "id": "<retailerId>" },
|
|
919
|
+
"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 }
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
"ref": "LOC_STORE_01",
|
|
923
|
+
"name": "CBD Store",
|
|
924
|
+
"type": "STORE",
|
|
925
|
+
"primaryAddress": {
|
|
926
|
+
"ref": "LOC_STORE_01_ADDR",
|
|
927
|
+
"name": "CBD Store",
|
|
928
|
+
"street": "200 Bourke Street",
|
|
929
|
+
"city": "Melbourne",
|
|
930
|
+
"state": "VIC",
|
|
931
|
+
"postcode": "3000",
|
|
932
|
+
"country": "AU",
|
|
933
|
+
"latitude": -37.8136,
|
|
934
|
+
"longitude": 144.9631
|
|
935
|
+
},
|
|
936
|
+
"retailer": { "id": "<retailerId>" },
|
|
937
|
+
"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 }
|
|
938
|
+
}
|
|
939
|
+
],
|
|
940
|
+
"returnFields": ["id", "ref", "name", "type", "status"]
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
**4. Create HD network + CC network:**
|
|
944
|
+
|
|
945
|
+
```json
|
|
946
|
+
{
|
|
947
|
+
"mutation": "createNetwork",
|
|
948
|
+
"inputs": [
|
|
949
|
+
{ "name": "NETWORK_HD", "type": "HD", "retailers": [{ "id": <retailerId> }] },
|
|
950
|
+
{ "name": "NETWORK_CC", "type": "CC", "retailers": [{ "id": <retailerId> }] }
|
|
951
|
+
],
|
|
952
|
+
"returnFields": ["id", "ref", "type", "status"]
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
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.
|
|
957
|
+
|
|
958
|
+
**5. Create product catalogue:**
|
|
959
|
+
|
|
960
|
+
```graphql
|
|
961
|
+
mutation($input: CreateProductCatalogueInput!) {
|
|
962
|
+
createProductCatalogue(input: $input) { id ref type status }
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
```json
|
|
966
|
+
{ "input": { "ref": "PC:MASTER:<RETAILER_REF>", "type": "MASTER", "name": "Master Catalogue", "retailerRefs": ["<RETAILER_REF>"] } }
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**6. Create 3 sample products (1 standard + 2 variants):**
|
|
970
|
+
|
|
971
|
+
Standard product first:
|
|
972
|
+
```graphql
|
|
973
|
+
mutation($input: CreateStandardProductInput!) {
|
|
974
|
+
createStandardProduct(input: $input) { id ref name status catalogue { ref } }
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
```json
|
|
978
|
+
{ "input": { "ref": "PROD_SAMPLE_001", "type": "STANDARD", "gtin": "PROD_SAMPLE_001", "name": "Sample Product", "catalogue": { "ref": "PC:MASTER:<RETAILER_REF>" } } }
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
Then variants via batch:
|
|
982
|
+
```json
|
|
983
|
+
{
|
|
984
|
+
"mutation": "createVariantProduct",
|
|
985
|
+
"inputs": [
|
|
986
|
+
{ "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>" } },
|
|
987
|
+
{ "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>" } }
|
|
988
|
+
],
|
|
989
|
+
"returnFields": ["id", "ref", "name", "status"]
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
**7. Create inventory positions (stock products at locations):**
|
|
994
|
+
|
|
995
|
+
Query inventory catalogue first: `{ inventoryCatalogues(first: 5) { edges { node { id ref type } } } }`. Use the catalogue ref (typically `DEFAULT:<retailerId>` or `COMPATIBILITY:<retailerId>`).
|
|
996
|
+
|
|
997
|
+
```json
|
|
998
|
+
{
|
|
999
|
+
"mutation": "createInventoryPosition",
|
|
1000
|
+
"inputs": [
|
|
1001
|
+
{ "ref": "SKU_SAMPLE_001_S", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_S", "locationRef": "LOC_WH_01", "onHand": 100 },
|
|
1002
|
+
{ "ref": "SKU_SAMPLE_001_M", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_M", "locationRef": "LOC_WH_01", "onHand": 100 },
|
|
1003
|
+
{ "ref": "SKU_SAMPLE_001_S", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_S", "locationRef": "LOC_STORE_01", "onHand": 25 },
|
|
1004
|
+
{ "ref": "SKU_SAMPLE_001_M", "type": "DEFAULT", "catalogue": { "ref": "DEFAULT:<retailerId>" }, "productRef": "SKU_SAMPLE_001_M", "locationRef": "LOC_STORE_01", "onHand": 25 }
|
|
1005
|
+
],
|
|
1006
|
+
"returnFields": ["id", "ref", "type", "status", "onHand"]
|
|
1007
|
+
}
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**8. Create virtual catalogue:**
|
|
1011
|
+
|
|
1012
|
+
```graphql
|
|
1013
|
+
mutation($input: CreateVirtualCatalogueInput!) {
|
|
1014
|
+
createVirtualCatalogue(input: $input) { id ref type status }
|
|
1015
|
+
}
|
|
1016
|
+
```
|
|
1017
|
+
```json
|
|
1018
|
+
{ "input": { "ref": "VC:DEFAULT:<RETAILER_REF>", "type": "DEFAULT", "name": "Default Virtual Catalogue", "inventoryCatalogueRef": "DEFAULT:<retailerId>", "productCatalogueRef": "PC:MASTER:<RETAILER_REF>", "retailerRefs": ["<RETAILER_REF>"] } }
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
**9. Create 1 test customer:**
|
|
1022
|
+
|
|
1023
|
+
```graphql
|
|
1024
|
+
mutation($input: CreateCustomerInput!) {
|
|
1025
|
+
createCustomer(input: $input) { id username firstName lastName primaryEmail }
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
```json
|
|
1029
|
+
{ "input": { "username": "test-customer-001", "firstName": "Test", "lastName": "Customer", "primaryEmail": "test@example.com", "promotionOptIn": false, "retailer": { "id": "<retailerId>" } } }
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
**10. Verify everything is discoverable:**
|
|
1033
|
+
|
|
1034
|
+
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.
|
|
1035
|
+
|
|
1036
|
+
## Batch Setup for Scale
|
|
1037
|
+
|
|
1038
|
+
For production-like environments with many entities, use `batch.create` / `batch.send` instead of individual mutations.
|
|
1039
|
+
|
|
1040
|
+
### Bulk Locations (100+)
|
|
1041
|
+
|
|
1042
|
+
```
|
|
1043
|
+
batch.create({ "name": "Bulk location load", "entityType": "LOCATION", "action": "UPSERT", "retailerId": "<retailerId>" })
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
Send records in batches of up to 500 per `batch.send` call. Each record follows the same shape as the `CreateLocationInput` variables.
|
|
1047
|
+
|
|
1048
|
+
### Bulk Products (1000+)
|
|
1049
|
+
|
|
1050
|
+
```
|
|
1051
|
+
batch.create({ "name": "Bulk product load", "entityType": "STANDARD_PRODUCT", "action": "UPSERT", "retailerId": "<retailerId>" })
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
For variant products, use a separate batch job with `entityType: "VARIANT_PRODUCT"`. Standard products must be loaded first since variants depend on them.
|
|
1055
|
+
|
|
1056
|
+
### Bulk Inventory (10000+)
|
|
1057
|
+
|
|
1058
|
+
```
|
|
1059
|
+
batch.create({ "name": "Bulk inventory load", "entityType": "INVENTORY_POSITION", "action": "UPSERT", "retailerId": "<retailerId>" })
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Inventory batch payloads need `ref`, `productRef`, `locationRef`, and `onHand` at minimum.
|
|
1063
|
+
|
|
1064
|
+
### Batch Workflow
|
|
1065
|
+
|
|
1066
|
+
```
|
|
1067
|
+
1. batch.create -> get jobId
|
|
1068
|
+
2. batch.send -> send records (repeat for each page of data)
|
|
1069
|
+
3. batch.status -> poll until terminal (COMPLETE or FAILED)
|
|
1070
|
+
4. batch.results -> inspect per-record success/failure
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
For multi-entity setups, create separate batch jobs per entity type and run them in dependency order (locations before networks, products before inventory).
|
|
1074
|
+
|
|
1075
|
+
## Integration with Other Skills
|
|
1076
|
+
|
|
1077
|
+
| Task | Skill |
|
|
1078
|
+
|------|-------|
|
|
1079
|
+
| Discover existing environment entities | `/fluent-test-data` |
|
|
1080
|
+
| Create test orders and fulfilments | `/fluent-test-data` |
|
|
1081
|
+
| Run E2E tests after setup | `/fluent-e2e-test` |
|
|
1082
|
+
| Configure settings (consignment prefix, webhooks) | `/fluent-settings` |
|
|
1083
|
+
| Build and deploy workflows | `/fluent-workflow-builder` |
|
|
1084
|
+
| Trace event failures during setup | `/fluent-trace` |
|
|
1085
|
+
| Analyze workflow prerequisites | `/fluent-workflow-analyzer` |
|
|
1086
|
+
| MCP tool syntax reference | `/fluent-mcp-tools` |
|
|
1087
|
+
|
|
1088
|
+
## Troubleshooting
|
|
1089
|
+
|
|
1090
|
+
| Problem | Cause | Fix |
|
|
1091
|
+
|---------|-------|-----|
|
|
1092
|
+
| `createLocation` fails with schema error | Input fields differ by Fluent version | Use `graphql.introspect` with `type: "CreateLocationInput"` to discover exact fields |
|
|
1093
|
+
| Network created but orders don't route | Locations not linked to network | Verify location-network assignment via location query with `networks` connection |
|
|
1094
|
+
| Products created but not found in catalogue | Wrong `catalogue.ref` in product creation | Query `productCatalogues` first and use exact ref format |
|
|
1095
|
+
| 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 |
|
|
1096
|
+
| Virtual catalogue empty | Virtual positions not created | Create virtual positions linking products to the virtual catalogue |
|
|
1097
|
+
| Duplicate ref error | Entity with same ref already exists | Query existing entities first or use `UPSERT` action in batch |
|
|
1098
|
+
| Mutation rejected with permission error | User lacks retailer-level permissions | Check user roles via `connection.test` or query `me { roles }` |
|
|
1099
|
+
|
|
1100
|
+
## Tips
|
|
1101
|
+
|
|
1102
|
+
- **Always use `graphql.introspect` to discover exact input types** — field names and required fields vary by Fluent version and account configuration.
|
|
1103
|
+
- **Use unique refs with timestamps for test environments** — Avoid collisions with existing data by appending `_<YYYYMMDD_HHmmss>` to refs.
|
|
1104
|
+
- **Location addresses need latitude and longitude** — Distance-based routing will not work without coordinates. Use a geocoding service if needed.
|
|
1105
|
+
- **Network-location assignment is critical** — Orders will not route to locations that are not assigned to the appropriate fulfilment network. Always verify after setup.
|
|
1106
|
+
- **Inventory positions need both `ref` (product SKU) and `locationRef`** — The combination must be unique. Missing either field will cause the mutation to fail.
|
|
1107
|
+
- **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.
|
|
1108
|
+
- **Virtual catalogues are optional but needed for fulfilment options and checkout** — If the retailer uses the fulfilment options API, virtual catalogues must exist.
|
|
1109
|
+
- **Create entities in dependency order** — Follow the configuration sequence table strictly. Out-of-order creation will result in reference errors.
|
|
1110
|
+
- **Schema varies by account** — Always use `graphql.introspect` if a mutation fails. Custom modules can add or modify input types.
|
|
1111
|
+
- **Run `/fluent-test-data` discovery after setup** — This confirms all entities are queryable and validates the environment is workflow-ready.
|