@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,385 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fluent-rule-scaffold
|
|
3
|
+
description: Scaffold new Fluent Commerce Rubix rules. Generates Java class, test class, and wires into module.json. Triggers on "create rule", "new rule", "scaffold rule", "add rule class".
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
|
6
|
+
argument-hint: <rule-name> [--entity-type ORDER|FULFILMENT|...] [--package common|fulfilment|order|...]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Rule Scaffolder
|
|
10
|
+
|
|
11
|
+
Generate new Fluent Commerce Rubix rule classes with proper structure, annotations, test skeletons, and module wiring.
|
|
12
|
+
|
|
13
|
+
## Planning Gate
|
|
14
|
+
|
|
15
|
+
**Before scaffolding any rule, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.** Every table row must carry a Source column (NEW/EXISTING/MODIFIED/REUSED/OOTB). For NEW rules, provide full pseudo logic.
|
|
16
|
+
|
|
17
|
+
**If this rule is part of a larger feature** (multiple rules + workflow changes + settings), use `/fluent-feature-plan` first to produce the comprehensive plan. Then reference the approved plan during scaffolding.
|
|
18
|
+
|
|
19
|
+
**Rule-scaffold specific emphasis — ensure these are covered:**
|
|
20
|
+
|
|
21
|
+
1. **Business Context (§1)** — what business need drives this rule; why custom, not OOTB
|
|
22
|
+
2. **State machine (§3.1)** — show the workflow status where this rule fires, with transitions
|
|
23
|
+
3. **Sequence diagram (§3.2)** — data flow: READ, VALIDATE, QUERY, BUILD, MUTATE, LOG. Use Mermaid `sequenceDiagram`. Validate syntax per `/fluent-mermaid-validate`
|
|
24
|
+
4. **Rulesets (§6)** — which rulesets use this rule, trigger event, from/to status
|
|
25
|
+
5. **Rules Inventory (§7)** — this rule classified as NEW with source label
|
|
26
|
+
6. **Detailed Design (§8)** — parameters table, pseudo logic, GraphQL operations
|
|
27
|
+
7. **Settings (§9)** — settings the rule reads, with context, value type, and shape
|
|
28
|
+
8. **GraphQL Operations (§12)** — queries and mutations, marked as NEW or REUSED pattern
|
|
29
|
+
|
|
30
|
+
**Write the plan to:** `accounts/<PROFILE>/plans/<YYYY-MM-DD>-rule-scaffold-<slug>.md`. Set `Status: PENDING`.
|
|
31
|
+
|
|
32
|
+
Present the full plan content to the user and wait for approval before generating any files. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).
|
|
33
|
+
|
|
34
|
+
## When to Use
|
|
35
|
+
|
|
36
|
+
- Creating a new custom Rubix rule
|
|
37
|
+
- Adding a rule that follows the project's BaseRule pattern
|
|
38
|
+
- Generating boilerplate with proper annotations and logging
|
|
39
|
+
- Wiring a new rule into `module.json`
|
|
40
|
+
|
|
41
|
+
## Pre-Check: Use OOTB or Build Custom?
|
|
42
|
+
|
|
43
|
+
Before scaffolding, determine if an out-of-the-box (OOTB) rule already covers the requirement.
|
|
44
|
+
|
|
45
|
+
### Decision Tree
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
User asks: "I need a rule that does X"
|
|
49
|
+
│
|
|
50
|
+
├── 1. Check module inventory
|
|
51
|
+
│ Read: accounts/<PROFILE>/analysis/module-validate/module-inventory.json
|
|
52
|
+
│ If missing → run /fluent-module-validate inventory first
|
|
53
|
+
│
|
|
54
|
+
├── 2. Search registered rules for matching capability
|
|
55
|
+
│ Use MCP tool: plugin.list with name filter
|
|
56
|
+
│ Look for rules whose description matches the ask
|
|
57
|
+
│ Check both FLUENTRETAIL.* (platform) and <ACCOUNT>.* (deployed)
|
|
58
|
+
│
|
|
59
|
+
├── 3. Evaluate match
|
|
60
|
+
│ ├── EXACT MATCH → Recommend OOTB rule
|
|
61
|
+
│ │ Tell user: "Use <RuleName> with parameters: ..."
|
|
62
|
+
│ │ Show accepted entity types and parameters
|
|
63
|
+
│ │ No scaffolding needed
|
|
64
|
+
│ │
|
|
65
|
+
│ ├── PARTIAL MATCH → Evaluate gap
|
|
66
|
+
│ │ Small gap (missing one parameter) → Consider extending OOTB
|
|
67
|
+
│ │ Large gap (fundamentally different) → Build custom
|
|
68
|
+
│ │
|
|
69
|
+
│ └── NO MATCH → Build custom rule
|
|
70
|
+
│ Proceed with scaffolding below
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### When OOTB Rules Suffice
|
|
74
|
+
|
|
75
|
+
| Need | OOTB Rule | Why not custom? |
|
|
76
|
+
|------|-----------|----------------|
|
|
77
|
+
| Change entity status | `SendEvent` / `ChangeStateGQL` | Standard lifecycle — no custom logic needed |
|
|
78
|
+
| Send event to another entity | `SendEventToEntity` | Platform handles entity resolution |
|
|
79
|
+
| Forward event conditionally | `ForwardIf*` family (dozens exist) | Many condition checks already built |
|
|
80
|
+
| Update attributes from event | `SetAttributes` / `UpsertAttributes` | Standard attribute operations |
|
|
81
|
+
| Create a fulfilment | `CreateFulfilmentRule` | Standard fulfilment creation |
|
|
82
|
+
|
|
83
|
+
### When Custom Rules Are Needed
|
|
84
|
+
|
|
85
|
+
| Need | Why custom? | Real example |
|
|
86
|
+
|------|------------|-------------|
|
|
87
|
+
| Setting-driven dynamic behavior | OOTB rules have fixed parameters, can't read settings for runtime config | `SendWebhookWithDynamicAttributes`, `UpdateEntityFromSetting` |
|
|
88
|
+
| Complex data transformation | Multi-step JSON path extraction + field mapping | `UpsertAttributeFromPath`, `UpdateFulfilmentItemsFromEvent` |
|
|
89
|
+
| Cross-entity composition | Query one entity to create/update another atomically | `CreateFulfilmentFromSourcingLocation` |
|
|
90
|
+
| Domain-specific validation | Business logic not covered by generic conditions | `CreateMissingVariantProducts` |
|
|
91
|
+
|
|
92
|
+
### Rule Discovery via MCP
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
# Search all registered rules by keyword
|
|
96
|
+
plugin.list name="Fulfilment" → all fulfilment-related rules
|
|
97
|
+
plugin.list name="SendEvent" → all event-forwarding rules
|
|
98
|
+
plugin.list name="Attribute" → all attribute manipulation rules
|
|
99
|
+
plugin.list name="Webhook" → webhook-related rules
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each result includes `description`, `parameters[]`, and `accepts[]` (entity types) — enough to determine fit.
|
|
103
|
+
|
|
104
|
+
## Rubix Rule Framework
|
|
105
|
+
|
|
106
|
+
All custom rules in this project follow this architecture:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
BaseRule (abstract)
|
|
110
|
+
└── extends Rule (Rubix SDK)
|
|
111
|
+
└── wraps Context in ContextWrapper
|
|
112
|
+
└── provides automatic logging via CommonUtils.generateLog()
|
|
113
|
+
└── subclasses implement: run(ContextWrapper context)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Required Annotations
|
|
117
|
+
|
|
118
|
+
```java
|
|
119
|
+
@RuleInfo(
|
|
120
|
+
name = "MyRuleName",
|
|
121
|
+
description = "What this rule does in one sentence"
|
|
122
|
+
)
|
|
123
|
+
@ParamString(name = "paramName", description = "What this parameter controls")
|
|
124
|
+
@Slf4j // Lombok logging
|
|
125
|
+
public class MyRuleName extends BaseRule {
|
|
126
|
+
@Override
|
|
127
|
+
public void run(ContextWrapper context) {
|
|
128
|
+
// Rule implementation
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Key APIs
|
|
134
|
+
|
|
135
|
+
| API | Usage |
|
|
136
|
+
|-----|-------|
|
|
137
|
+
| `context.getProp("name")` | Read ruleset prop value |
|
|
138
|
+
| `context.getEvent()` | Get the triggering event |
|
|
139
|
+
| `context.getEvent().getAccountId()` | Account ID for logging |
|
|
140
|
+
| `context.getEvent().getEntityId()` | Entity ID |
|
|
141
|
+
| `context.getEvent().getEntityRef()` | Entity reference |
|
|
142
|
+
| `context.getEvent().getEntityType()` | Entity type |
|
|
143
|
+
| `context.getEvent().getAttributes()` | Event attributes map |
|
|
144
|
+
| `context.addLog("message")` | Add to execution log |
|
|
145
|
+
| `context.action().mutation()` | Queue a GraphQL mutation (executes AFTER rule completes) |
|
|
146
|
+
| `context.action().eventAction()` | Queue an event to fire |
|
|
147
|
+
| `DynamicUtils.query(context, paths, queryName, params)` | Execute dynamic GraphQL query |
|
|
148
|
+
| `DynamicUtils.mutate(context, values)` | Execute dynamic mutation |
|
|
149
|
+
| `DynamicUtils.getJsonPath(context, jsonPath)` | Resolve value from event or entity |
|
|
150
|
+
| `SettingUtils.getSettingByRef(context, ref)` | Read a Fluent Setting |
|
|
151
|
+
|
|
152
|
+
## Rule Class Template
|
|
153
|
+
|
|
154
|
+
```java
|
|
155
|
+
package com.fluentcommerce.rule.<PACKAGE>;
|
|
156
|
+
|
|
157
|
+
import com.fluentcommerce.common.BaseRule;
|
|
158
|
+
import com.fluentcommerce.common.ContextWrapper;
|
|
159
|
+
import com.fluentretail.rubix.rule.meta.ParamString;
|
|
160
|
+
import com.fluentretail.rubix.rule.meta.RuleInfo;
|
|
161
|
+
import lombok.extern.slf4j.Slf4j;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* <DESCRIPTION>
|
|
165
|
+
*/
|
|
166
|
+
@RuleInfo(
|
|
167
|
+
name = "<RULE_NAME>",
|
|
168
|
+
description = "<DESCRIPTION>"
|
|
169
|
+
)
|
|
170
|
+
@ParamString(name = "<PARAM1>", description = "<PARAM1_DESC>")
|
|
171
|
+
@Slf4j
|
|
172
|
+
public class <RULE_NAME> extends BaseRule {
|
|
173
|
+
|
|
174
|
+
private static final String CLASS_NAME = <RULE_NAME>.class.getSimpleName();
|
|
175
|
+
|
|
176
|
+
@Override
|
|
177
|
+
public void run(ContextWrapper context) {
|
|
178
|
+
String accountId = context.getEvent().getAccountId();
|
|
179
|
+
|
|
180
|
+
log.info("[{}] [{}] - Processing event: {}", accountId, CLASS_NAME,
|
|
181
|
+
context.getEvent().getName());
|
|
182
|
+
context.addLog("Processing " + CLASS_NAME);
|
|
183
|
+
|
|
184
|
+
// Read props
|
|
185
|
+
String param1 = context.getProp("<PARAM1>");
|
|
186
|
+
if (param1 == null || param1.trim().isEmpty()) {
|
|
187
|
+
log.error("[{}] [{}] - Required prop '<PARAM1>' is missing", accountId, CLASS_NAME);
|
|
188
|
+
context.addLog("Error: <PARAM1> is required");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// === Rule logic here ===
|
|
194
|
+
|
|
195
|
+
context.addLog(CLASS_NAME + " completed successfully");
|
|
196
|
+
} catch (Exception e) {
|
|
197
|
+
log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
|
|
198
|
+
context.addLog("Error in " + CLASS_NAME + ": " + e.getMessage());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Test Class Template
|
|
205
|
+
|
|
206
|
+
```java
|
|
207
|
+
package com.fluentcommerce.rule.<PACKAGE>;
|
|
208
|
+
|
|
209
|
+
import com.fluentretail.rubix.event.Event;
|
|
210
|
+
import com.fluentretail.rubix.rule.meta.RuleInfo;
|
|
211
|
+
import com.fluentretail.rubix.v2.context.Context;
|
|
212
|
+
import com.fluentretail.rubix.v2.action.Action;
|
|
213
|
+
import com.fluentretail.rubix.v2.action.MutationAction;
|
|
214
|
+
import com.fluentretail.rubix.v2.action.EventAction;
|
|
215
|
+
import org.junit.jupiter.api.BeforeEach;
|
|
216
|
+
import org.junit.jupiter.api.Test;
|
|
217
|
+
import org.junit.jupiter.api.DisplayName;
|
|
218
|
+
import org.mockito.Mock;
|
|
219
|
+
import org.mockito.MockitoAnnotations;
|
|
220
|
+
|
|
221
|
+
import java.util.HashMap;
|
|
222
|
+
import java.util.Map;
|
|
223
|
+
|
|
224
|
+
import static org.junit.jupiter.api.Assertions.*;
|
|
225
|
+
import static org.mockito.Mockito.*;
|
|
226
|
+
|
|
227
|
+
class <RULE_NAME>Test {
|
|
228
|
+
|
|
229
|
+
private <RULE_NAME> rule;
|
|
230
|
+
|
|
231
|
+
@Mock private Context context;
|
|
232
|
+
@Mock private Event event;
|
|
233
|
+
@Mock private Action action;
|
|
234
|
+
@Mock private MutationAction mutationAction;
|
|
235
|
+
@Mock private EventAction eventAction;
|
|
236
|
+
|
|
237
|
+
@BeforeEach
|
|
238
|
+
void setUp() {
|
|
239
|
+
MockitoAnnotations.openMocks(this);
|
|
240
|
+
rule = new <RULE_NAME>();
|
|
241
|
+
|
|
242
|
+
when(context.getEvent()).thenReturn(event);
|
|
243
|
+
when(context.action()).thenReturn(action);
|
|
244
|
+
when(action.mutation()).thenReturn(mutationAction);
|
|
245
|
+
when(action.eventAction()).thenReturn(eventAction);
|
|
246
|
+
when(event.getAccountId()).thenReturn("TEST_ACCOUNT");
|
|
247
|
+
when(event.getName()).thenReturn("TestEvent");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Test
|
|
251
|
+
@DisplayName("Rule has correct @RuleInfo annotation")
|
|
252
|
+
void hasRuleInfo() {
|
|
253
|
+
RuleInfo info = <RULE_NAME>.class.getAnnotation(RuleInfo.class);
|
|
254
|
+
assertNotNull(info);
|
|
255
|
+
assertEquals("<RULE_NAME>", info.name());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Test
|
|
259
|
+
@DisplayName("Handles missing required prop gracefully")
|
|
260
|
+
void missingRequiredProp() {
|
|
261
|
+
Map<String, String> props = new HashMap<>();
|
|
262
|
+
when(context.getProp("<PARAM1>")).thenReturn(null);
|
|
263
|
+
|
|
264
|
+
// Should not throw — rule logs error and returns
|
|
265
|
+
assertDoesNotThrow(() -> rule.run(context));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@Test
|
|
269
|
+
@DisplayName("Processes event successfully with valid props")
|
|
270
|
+
void processesSuccessfully() {
|
|
271
|
+
when(context.getProp("<PARAM1>")).thenReturn("testValue");
|
|
272
|
+
|
|
273
|
+
rule.run(context);
|
|
274
|
+
|
|
275
|
+
// Verify expected behavior
|
|
276
|
+
// verify(mutationAction).xxx(...);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Pre-Check: Load Source Analysis
|
|
282
|
+
|
|
283
|
+
Before scaffolding, check if `/fluent-custom-code` analysis artifacts exist:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
accounts/<PROFILE>/analysis/custom-code/source-map.json
|
|
287
|
+
accounts/<PROFILE>/analysis/custom-code/constraints.json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
If found, read them to discover:
|
|
291
|
+
- `modules[].patterns.basePackage` — correct package prefix
|
|
292
|
+
- `modules[].patterns.subPackages` — available sub-packages
|
|
293
|
+
- `modules[].patterns.baseClass` — base class to extend
|
|
294
|
+
- `modules[].buildRoot` — where to run Maven
|
|
295
|
+
- `modules[].rules[]` — existing rules (avoid name collisions)
|
|
296
|
+
- `constraints[].extensionConstraints` — rules to follow
|
|
297
|
+
|
|
298
|
+
If not found, suggest running `/fluent-custom-code <PROFILE>` first, or fall back to the defaults below. Treat artifact-gate failures as "not found" (for example missing required files, missing hashes, or blocking `missingSources` in `constraints.json`).
|
|
299
|
+
|
|
300
|
+
## Scaffolding Steps
|
|
301
|
+
|
|
302
|
+
### 1. Determine rule location
|
|
303
|
+
|
|
304
|
+
Rules live under the plugin source tree:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
plugins/rules/<module-alias>/src/main/java/com/fluentcommerce/rule/
|
|
308
|
+
├── common/ # Entity-agnostic rules
|
|
309
|
+
├── fulfilment/ # Fulfilment-specific rules
|
|
310
|
+
├── variantproduct/ # Product-specific rules
|
|
311
|
+
├── order/ # Order-specific rules (create if needed)
|
|
312
|
+
└── returnorder/ # Return order rules (create if needed)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 2. Create the rule class
|
|
316
|
+
|
|
317
|
+
Write the Java file at the appropriate package path using the template above. Replace all `<PLACEHOLDERS>`.
|
|
318
|
+
|
|
319
|
+
### 3. Create the test class
|
|
320
|
+
|
|
321
|
+
Write the test at the mirror path under `src/test/java/`. Test class name = `<RuleName>Test.java`.
|
|
322
|
+
|
|
323
|
+
### 4. Wire into module.json
|
|
324
|
+
|
|
325
|
+
Add the rule name to the `rules` array in the module's `resources/module.json`:
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"rules": [
|
|
330
|
+
"ExistingRule1",
|
|
331
|
+
"ExistingRule2",
|
|
332
|
+
"NewRuleName"
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 5. Verify compilation
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
cd plugins/ && mvn clean install -pl rules/<module-alias> -am
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
If Maven fails, check:
|
|
344
|
+
- Node.js version (must be 18 for Apollo codegen): `nvm use 18` on Unix; on Windows with nvm-windows (nvm4w), use the full version number: `nvm use 18.x.x`
|
|
345
|
+
- Import paths are correct
|
|
346
|
+
- `BaseRule` and `ContextWrapper` are in the `common` package
|
|
347
|
+
|
|
348
|
+
## Rules That Use Dynamic GraphQL
|
|
349
|
+
|
|
350
|
+
For rules that need to query or mutate entities dynamically:
|
|
351
|
+
|
|
352
|
+
```java
|
|
353
|
+
import com.fluentcommerce.util.dynamic.DynamicUtils;
|
|
354
|
+
|
|
355
|
+
// Query entity fields via JSON paths
|
|
356
|
+
List<String> paths = Arrays.asList("ref", "status", "fulfilments.ref", "fulfilments.status");
|
|
357
|
+
JsonNode result = DynamicUtils.query(context, paths, "orderQuery", null);
|
|
358
|
+
|
|
359
|
+
// Mutate entity
|
|
360
|
+
Map<String, Object> values = new HashMap<>();
|
|
361
|
+
values.put("id", entityId);
|
|
362
|
+
values.put("status", "NEW_STATUS");
|
|
363
|
+
values.put("attributes", attributeList);
|
|
364
|
+
DynamicUtils.mutate(context, values);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Path syntax:** Omit `edges` and `node` — DynamicEntityQuery adds them automatically. Use `items.product.name` not `items.edges.node.product.name`.
|
|
368
|
+
|
|
369
|
+
## Rules That Read Settings
|
|
370
|
+
|
|
371
|
+
```java
|
|
372
|
+
import com.fluentcommerce.util.SettingUtils;
|
|
373
|
+
import com.fluentretail.rubix.foundation.graphql.type.Setting;
|
|
374
|
+
|
|
375
|
+
Setting setting = SettingUtils.getSettingByRef(context, "my.setting.ref");
|
|
376
|
+
JsonNode config = JsonUtils.stringToPojo(setting.getLobValue(), JsonNode.class);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Gotchas
|
|
380
|
+
|
|
381
|
+
- **`@ParamString` names are CASE-SENSITIVE** — `"jsonpath"` and `"jsonPath"` are different props
|
|
382
|
+
- **`context.action().mutation()` only QUEUES** — the mutation executes AFTER the rule returns
|
|
383
|
+
- **`DynamicMutation.buildMutationDocument` appends `!`** — never include `!` in type names
|
|
384
|
+
- **`updateFulfilmentItem` does NOT exist** — use `updateFulfilment` with nested `items` array
|
|
385
|
+
- **BaseRule wraps Context** — implement `run(ContextWrapper context)`, not `run(Context context)`
|