@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.mjs +1973 -0
  4. package/content/cli/agents/fluent-cli/agent.json +149 -0
  5. package/content/cli/agents/fluent-cli.md +132 -0
  6. package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
  7. package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
  8. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
  9. package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
  10. package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
  11. package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
  12. package/content/cli/skills/fluent-connect/SKILL.md +886 -0
  13. package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
  14. package/content/cli/skills/fluent-profile/SKILL.md +180 -0
  15. package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
  16. package/content/dev/agents/fluent-dev/agent.json +88 -0
  17. package/content/dev/agents/fluent-dev.md +525 -0
  18. package/content/dev/reference-modules/catalog.json +4754 -0
  19. package/content/dev/skills/fluent-build/SKILL.md +192 -0
  20. package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
  21. package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
  22. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
  23. package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
  24. package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
  25. package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
  26. package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
  27. package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
  28. package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
  29. package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
  30. package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
  31. package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
  32. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
  33. package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
  34. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
  35. package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
  36. package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
  37. package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
  38. package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
  39. package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
  40. package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
  41. package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
  42. package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
  43. package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
  44. package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
  45. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
  46. package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
  47. package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
  48. package/content/mcp-extn/agents/fluent-mcp.md +69 -0
  49. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
  50. package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
  51. package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
  52. package/content/rfl/agents/fluent-rfl.md +56 -0
  53. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
  54. package/docs/CAPABILITY_MAP.md +77 -0
  55. package/docs/CLI_COVERAGE.md +47 -0
  56. package/docs/DEV_WORKFLOW.md +802 -0
  57. package/docs/FLOW_RUN.md +142 -0
  58. package/docs/USE_CASES.md +404 -0
  59. package/metadata.json +156 -0
  60. 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.