@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,1928 @@
1
+ ---
2
+ name: fluent-module-scaffold
3
+ description: Scaffold a new Fluent Commerce extension module. Generates Maven project structure, module.json, build scripts, initial rule classes, test skeletons, and .gitignore. Triggers on "scaffold module", "new module", "create module", "initialize module".
4
+ user-invocable: true
5
+ allowed-tools: Bash, Read, Write, Edit, Glob, Grep
6
+ argument-hint: <module-name> [--entity-types ORDER,FULFILMENT] [--rules RuleName1,RuleName2] [--account-prefix ACCT]
7
+ ---
8
+
9
+ # Module Scaffolder
10
+
11
+ Generate a complete, buildable Fluent Commerce extension module skeleton from a module name, entity scope, and optional rule list. The output is a directory under `accounts/<PROFILE>/SOURCE/` that passes `/fluent-module-validate` and `/fluent-build` on first run.
12
+
13
+ ## Pre-Check: New Module or Extend Existing?
14
+
15
+ **ALWAYS run this decision tree before scaffolding. Do NOT skip.**
16
+
17
+ ```
18
+ User asks: "I need a module for X" / "Build me a module" / "Create rules for X"
19
+
20
+ ├── 1. Discover existing modules in workspace
21
+ │ Search: accounts/<PROFILE>/SOURCE/*/resources/module.json
22
+ │ Also check: accounts/<PROFILE>/analysis/custom-code/source-map.json
23
+ │ List found modules with their rules and entity types
24
+
25
+ ├── 2. Check deployed modules in live environment
26
+ │ Use MCP tool: plugin.list (no filter — get all custom rules)
27
+ │ Look for <ACCOUNT>.* rules (not FLUENTRETAIL.*)
28
+ │ Cross-reference with local SOURCE/ to find modules with source code available
29
+
30
+ ├── 3. Evaluate: does the new functionality fit an existing module?
31
+ │ ├── YES — functionality belongs in an existing module's domain
32
+ │ │ ├── Source code in SOURCE/? → Use /fluent-rule-scaffold to add rules
33
+ │ │ ├── No source code? → Ask user to clone the repo first
34
+ │ │ │ "Module <name> is deployed but source not found in accounts/<PROFILE>/SOURCE/"
35
+ │ │ │ "Please clone the repo: git clone <url> accounts/<PROFILE>/SOURCE/<repo>"
36
+ │ │ │ Then use /fluent-custom-code to analyze, then /fluent-rule-scaffold
37
+ │ │ └── STOP — do NOT scaffold a new module
38
+ │ │
39
+ │ ├── NO — genuinely new domain, no existing module covers it
40
+ │ │ └── Proceed with scaffolding below
41
+ │ │
42
+ │ └── UNCLEAR — could go either way
43
+ │ └── Ask user: "Should I add rules to existing module <X> or create a new module?"
44
+ ```
45
+
46
+ ### Source Code Availability Check
47
+
48
+ Before extending an existing module, verify the source is accessible:
49
+
50
+ | Source State | Action |
51
+ |---|---|
52
+ | `accounts/<PROFILE>/SOURCE/<repo>/` exists with `pom.xml` + `module.json` | Ready — use `/fluent-rule-scaffold` |
53
+ | Module deployed but no source in `SOURCE/` | Ask user to clone repo into `SOURCE/` |
54
+ | Source exists but no `source-map.json` analysis | Run `/fluent-custom-code` first to map packages, base classes, conventions |
55
+ | Source exists with stale `source-map.json` | Re-run `/fluent-custom-code` to refresh |
56
+
57
+ ## Planning Gate
58
+
59
+ **After the pre-check determines a NEW module is needed, 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).
60
+
61
+ **Module-scaffold specific emphasis — ensure these are covered:**
62
+
63
+ 1. **Business Context (Section 1)** — why a new module (not extending existing), decision rationale from pre-check
64
+ 2. **Entity relationship diagram (Section 3)** — Mermaid diagram showing entity types the module operates on and their edges. Validate syntax per `/fluent-mermaid-validate`
65
+ 3. **Cross-entity flow (Section 3.2)** — Mermaid `sequenceDiagram` showing how the module's rules will be triggered and what events/mutations they produce. Validate syntax per `/fluent-mermaid-validate`
66
+ 4. **Impacted workflows (Section 4.1)** — which workflow rulesets will use these rules (existing or new)
67
+ 5. **Rulesets & rules (Section 4.3)** — initial rules table with entity type, trigger event, OOTB/custom, inline/scheduled, description, parameters
68
+ 6. **Settings (Section 4.4)** — settings the rules will need, with context and expected format
69
+ 7. **GraphQL operations (Section 4.7)** — queries and mutations the rules will execute
70
+ 8. **Detailed Design (Section 5)** — Maven directory structure, SDK version, parent POM, dependencies, package naming convention
71
+
72
+ **Write the plan to:** `accounts/<PROFILE>/plans/<YYYY-MM-DD>-module-scaffold-<slug>.md`. Set `Status: PENDING`.
73
+
74
+ 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).
75
+
76
+ ## When to Use
77
+
78
+ - Creating a brand-new Fluent Commerce extension module from scratch
79
+ - Starting a greenfield project that needs a complete Maven + module.json + build scripts structure
80
+ - Bootstrapping a module for a new account or retailer where no custom code exists yet
81
+ - Generating an empty module shell to be populated incrementally with `/fluent-rule-scaffold`
82
+
83
+ ## Ownership Boundary
84
+
85
+ This skill owns:
86
+
87
+ - Creating the complete module directory structure
88
+ - Generating the parent POM and all submodule POMs (types, util, rules)
89
+ - Generating `module.json` with correct Fluent Commerce manifest format
90
+ - Generating initial rule classes and test skeletons (if `--rules` specified)
91
+ - Generating build scripts (bash `.sh` and PowerShell `.ps1`)
92
+ - Generating `.gitignore`
93
+ - Account prefix detection for rule registration
94
+
95
+ This skill does **not** own:
96
+
97
+ - Adding rules to an existing module --> `/fluent-rule-scaffold`
98
+ - Building and packaging --> `/fluent-build`
99
+ - Validating module structure --> `/fluent-module-validate`
100
+ - Deploying modules --> `/fluent-module-deploy`
101
+ - Analyzing existing custom code --> `/fluent-custom-code`
102
+
103
+ ## Inputs
104
+
105
+ | Parameter | Required | Default | Description |
106
+ |-----------|----------|---------|-------------|
107
+ | `module-name` | Yes | -- | Module identifier (e.g., `hm-returns`). Used for directory names, POM artifactId, module.json name. Must be lowercase alphanumeric with hyphens only. |
108
+ | `--entity-types` | No | `ORDER` | Comma-separated entity types the module's rules will operate on. Valid: `ORDER`, `FULFILMENT`, `FULFILMENT_OPTIONS`, `ARTICLE`, `CONSIGNMENT`, `WAVE`, `LOCATION`, `PRODUCT`, `VIRTUAL_CATALOGUE`, `INVENTORY_POSITION`, `BILLING_ACCOUNT`, `RETURN_ORDER` |
109
+ | `--rules` | No | -- | Comma-separated rule class names to scaffold (each gets a Java class + test). Names must be PascalCase. |
110
+ | `--account-prefix` | No | Auto-detect from profile | Account context prefix for rule registration in module.json (e.g., `HMDEV`). Used as the OSGi symbolic name prefix. |
111
+ | `--group-id` | No | `com.fluentcommerce` | Maven groupId for all POMs |
112
+ | `--initial-version` | No | `1.0.0-SNAPSHOT` | Initial POM and module.json version |
113
+ | `--profile` | No | Active profile | Fluent CLI profile for account prefix detection and module path resolution |
114
+
115
+ ## Pre-Check: Module Already Exists?
116
+
117
+ Before creating anything, check if the target directory already exists:
118
+
119
+ ```
120
+ accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-<module-name>/
121
+ ```
122
+
123
+ If it exists, **abort** with message:
124
+ ```
125
+ Module directory already exists: accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-<module-name>/
126
+ Use /fluent-rule-scaffold to add rules to an existing module, or choose a different module name.
127
+ ```
128
+
129
+ Also check for name collisions against deployed modules:
130
+
131
+ ```
132
+ # Query deployed modules via Fluent CLI
133
+ fluent module list -p <PROFILE>
134
+ ```
135
+
136
+ If a module with the same name is already deployed, warn (but do not abort -- the user may be re-creating local source for an existing deployed module).
137
+
138
+ ## Account Prefix Detection
139
+
140
+ The account prefix is used in `module.json` rule registration keys and the OSGi symbolic name. Detection order:
141
+
142
+ ```
143
+ 1. If --account-prefix provided, use it directly
144
+
145
+ 2. Else query registered rules via MCP:
146
+ plugin.list (compact: true)
147
+ → Parse first custom rule key: "<ACCOUNT>.<context>.<RuleName>"
148
+ → Extract <ACCOUNT> prefix (the part before the first dot that is NOT "FLUENTRETAIL")
149
+
150
+ 3. Else derive from PROFILE name:
151
+ HMDEV → HMDEV
152
+ SAGIRISH → SAGIRISH
153
+ (uppercase the profile name)
154
+
155
+ 4. Fallback: "UNKNOWN"
156
+ → Warn user: "Could not detect account prefix. Update module.json manually."
157
+ ```
158
+
159
+ ### Module Alias Derivation
160
+
161
+ The `<module-alias>` is derived from `<module-name>` by removing the common prefix pattern. It is used for submodule directory names and artifact suffixes:
162
+
163
+ ```
164
+ module-name: hm-returns
165
+ module-alias: hm-returns
166
+
167
+ module-name: sagirish-extensions
168
+ module-alias: sagirish-extensions
169
+ ```
170
+
171
+ The alias is the same as the module name. It appears in paths like `plugins/rules/<module-alias>/`.
172
+
173
+ ### OSGi Symbolic Name
174
+
175
+ Derived from the account prefix and module alias with special characters removed:
176
+
177
+ ```
178
+ account-prefix: HMDEV
179
+ module-alias: hm-returns
180
+ osgi-name: _.hmreturns
181
+
182
+ account-prefix: SAGIRISH
183
+ module-alias: sagirish-extensions
184
+ osgi-name: _.sagirishextensions
185
+ ```
186
+
187
+ Pattern: `_.<alias-with-hyphens-removed>`
188
+
189
+ ## Generated Directory Structure
190
+
191
+ ```
192
+ accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-<module-name>/
193
+ +-- plugins/
194
+ | +-- pom.xml # Parent POM (multi-module, packaging=pom)
195
+ | +-- types/
196
+ | | +-- pom.xml # Types aggregator POM (packaging=pom)
197
+ | | +-- <module-alias>/
198
+ | | +-- pom.xml # Types implementation POM
199
+ | | +-- src/
200
+ | | +-- main/java/com/fluentcommerce/types/ # (empty, ready for DTOs)
201
+ | +-- util/
202
+ | | +-- pom.xml # Util aggregator POM (packaging=pom)
203
+ | | +-- <module-alias>/
204
+ | | +-- pom.xml # Util implementation POM
205
+ | | +-- src/
206
+ | | +-- main/java/com/fluentcommerce/util/ # (empty, ready for helpers)
207
+ | +-- rules/
208
+ | +-- pom.xml # Rules aggregator POM (packaging=pom)
209
+ | +-- <module-alias>/
210
+ | +-- pom.xml # Rules implementation POM (OSGi bundle)
211
+ | +-- src/
212
+ | +-- main/java/com/fluentcommerce/rule/
213
+ | | +-- <package>/ # Sub-package per entity type
214
+ | | +-- <RuleName1>.java # (if --rules specified)
215
+ | | +-- <RuleName2>.java
216
+ | +-- test/java/com/fluentcommerce/rule/
217
+ | +-- <package>/
218
+ | +-- <RuleName1>Test.java # (if --rules specified)
219
+ | +-- <RuleName2>Test.java
220
+ +-- resources/
221
+ | +-- module.json # Fluent Commerce module manifest
222
+ | +-- settings/ # (empty, ready for default settings JSON)
223
+ +-- global/ # (empty, for GraphQL schema.json)
224
+ +-- scripts/
225
+ | +-- build-module.sh # Bash build + packaging script
226
+ | +-- build-module.ps1 # PowerShell build + packaging script
227
+ +-- dist/ # (empty, output dir for ZIP artifacts)
228
+ +-- .gitignore
229
+ ```
230
+
231
+ ### POM Hierarchy
232
+
233
+ The Maven project uses a 3-level POM hierarchy matching real Fluent Commerce modules:
234
+
235
+ ```
236
+ plugins/pom.xml (parent, packaging=pom)
237
+ +-- types/pom.xml (aggregator, packaging=pom)
238
+ | +-- types/<alias>/pom.xml (implementation)
239
+ +-- util/pom.xml (aggregator, packaging=pom)
240
+ | +-- util/<alias>/pom.xml (implementation)
241
+ +-- rules/pom.xml (aggregator, packaging=pom)
242
+ +-- rules/<alias>/pom.xml (implementation, OSGi bundle)
243
+ ```
244
+
245
+ The rules implementation POM is the only one that produces a deployable JAR. It depends on the types and util artifacts.
246
+
247
+ ## File Templates
248
+
249
+ ### Parent POM (`plugins/pom.xml`)
250
+
251
+ ```xml
252
+ <?xml version="1.0" encoding="UTF-8"?>
253
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
254
+ xmlns="http://maven.apache.org/POM/4.0.0"
255
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
256
+ <modelVersion>4.0.0</modelVersion>
257
+
258
+ <groupId>${GROUP_ID}</groupId>
259
+ <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
260
+ <version>${INITIAL_VERSION}</version>
261
+ <packaging>pom</packaging>
262
+
263
+ <name>FC Module ${MODULE_TITLE} - Plugins Parent</name>
264
+ <description>Parent POM for fc-module-${MODULE_NAME} plugin builds</description>
265
+
266
+ <modules>
267
+ <module>types</module>
268
+ <module>util</module>
269
+ <module>rules</module>
270
+ </modules>
271
+
272
+ <properties>
273
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
274
+ <maven.compiler.source>1.8</maven.compiler.source>
275
+ <maven.compiler.target>1.8</maven.compiler.target>
276
+ <apollo-client-maven-plugin.version>1.0.0.15</apollo-client-maven-plugin.version>
277
+ </properties>
278
+
279
+ <build>
280
+ <pluginManagement>
281
+ <plugins>
282
+ <plugin>
283
+ <groupId>com.fluentcommerce</groupId>
284
+ <artifactId>apollo-client-maven-plugin</artifactId>
285
+ <version>${apollo-client-maven-plugin.version}</version>
286
+ <configuration>
287
+ <basePackage>com.fluentcommerce.graphql</basePackage>
288
+ <introspectionFile>${project.basedir}/../../../global/schema.json</introspectionFile>
289
+ </configuration>
290
+ <executions>
291
+ <execution>
292
+ <id>generate-classes</id>
293
+ <goals>
294
+ <goal>generate</goal>
295
+ </goals>
296
+ </execution>
297
+ <execution>
298
+ <id>replace-customtypes</id>
299
+ <goals>
300
+ <goal>replace</goal>
301
+ </goals>
302
+ </execution>
303
+ </executions>
304
+ <dependencies>
305
+ <dependency>
306
+ <groupId>com.squareup.okio</groupId>
307
+ <artifactId>okio</artifactId>
308
+ <version>1.17.6</version>
309
+ </dependency>
310
+ <dependency>
311
+ <groupId>com.squareup.okhttp3</groupId>
312
+ <artifactId>okhttp</artifactId>
313
+ <version>3.14.9</version>
314
+ </dependency>
315
+ </dependencies>
316
+ </plugin>
317
+ </plugins>
318
+ </pluginManagement>
319
+ </build>
320
+
321
+ </project>
322
+ ```
323
+
324
+ ### Types Aggregator POM (`plugins/types/pom.xml`)
325
+
326
+ ```xml
327
+ <?xml version="1.0" encoding="UTF-8"?>
328
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
329
+ xmlns="http://maven.apache.org/POM/4.0.0"
330
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
331
+ <modelVersion>4.0.0</modelVersion>
332
+
333
+ <parent>
334
+ <groupId>${GROUP_ID}</groupId>
335
+ <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
336
+ <version>${INITIAL_VERSION}</version>
337
+ </parent>
338
+
339
+ <artifactId>fc-module-${MODULE_NAME}-types</artifactId>
340
+ <packaging>pom</packaging>
341
+
342
+ <name>FC Module ${MODULE_TITLE} - Types</name>
343
+ <description>Parent POM for fc-module-${MODULE_NAME} types plugins</description>
344
+
345
+ <modules>
346
+ <module>${MODULE_ALIAS}</module>
347
+ </modules>
348
+
349
+ </project>
350
+ ```
351
+
352
+ ### Types Implementation POM (`plugins/types/<module-alias>/pom.xml`)
353
+
354
+ ```xml
355
+ <?xml version="1.0" encoding="UTF-8"?>
356
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
357
+ xmlns="http://maven.apache.org/POM/4.0.0"
358
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
359
+ <modelVersion>4.0.0</modelVersion>
360
+
361
+ <parent>
362
+ <groupId>${GROUP_ID}</groupId>
363
+ <artifactId>fc-module-${MODULE_NAME}-types</artifactId>
364
+ <version>${INITIAL_VERSION}</version>
365
+ </parent>
366
+
367
+ <artifactId>fc-types-${MODULE_NAME}</artifactId>
368
+
369
+ <name>FC Module ${MODULE_TITLE} - Types Implementation</name>
370
+ <description>DTOs and data types for ${MODULE_TITLE} module</description>
371
+
372
+ <properties>
373
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
374
+ <lombok.version>1.18.30</lombok.version>
375
+ <jackson.version>2.10.0</jackson.version>
376
+ </properties>
377
+
378
+ <dependencies>
379
+ <dependency>
380
+ <groupId>org.projectlombok</groupId>
381
+ <artifactId>lombok</artifactId>
382
+ <version>${lombok.version}</version>
383
+ <scope>provided</scope>
384
+ </dependency>
385
+
386
+ <dependency>
387
+ <groupId>com.fasterxml.jackson.core</groupId>
388
+ <artifactId>jackson-core</artifactId>
389
+ <version>${jackson.version}</version>
390
+ </dependency>
391
+
392
+ <dependency>
393
+ <groupId>com.fasterxml.jackson.core</groupId>
394
+ <artifactId>jackson-databind</artifactId>
395
+ <version>${jackson.version}</version>
396
+ </dependency>
397
+
398
+ <dependency>
399
+ <groupId>com.fasterxml.jackson.core</groupId>
400
+ <artifactId>jackson-annotations</artifactId>
401
+ <version>${jackson.version}</version>
402
+ </dependency>
403
+
404
+ <dependency>
405
+ <groupId>javax.annotation</groupId>
406
+ <artifactId>javax.annotation-api</artifactId>
407
+ <version>1.3.2</version>
408
+ <scope>provided</scope>
409
+ </dependency>
410
+ </dependencies>
411
+
412
+ <build>
413
+ <plugins>
414
+ <plugin>
415
+ <groupId>org.apache.maven.plugins</groupId>
416
+ <artifactId>maven-compiler-plugin</artifactId>
417
+ <version>3.11.0</version>
418
+ <configuration>
419
+ <target>1.8</target>
420
+ <source>1.8</source>
421
+ <fork>true</fork>
422
+ <compilerArgs>
423
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
424
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
425
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
426
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
427
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
428
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
429
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
430
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
431
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
432
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
433
+ </compilerArgs>
434
+ <annotationProcessorPaths>
435
+ <path>
436
+ <groupId>org.projectlombok</groupId>
437
+ <artifactId>lombok</artifactId>
438
+ <version>${lombok.version}</version>
439
+ </path>
440
+ </annotationProcessorPaths>
441
+ </configuration>
442
+ </plugin>
443
+ </plugins>
444
+ </build>
445
+ </project>
446
+ ```
447
+
448
+ ### Util Aggregator POM (`plugins/util/pom.xml`)
449
+
450
+ ```xml
451
+ <?xml version="1.0" encoding="UTF-8"?>
452
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
453
+ xmlns="http://maven.apache.org/POM/4.0.0"
454
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
455
+ <modelVersion>4.0.0</modelVersion>
456
+
457
+ <parent>
458
+ <groupId>${GROUP_ID}</groupId>
459
+ <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
460
+ <version>${INITIAL_VERSION}</version>
461
+ </parent>
462
+
463
+ <artifactId>fc-module-${MODULE_NAME}-util</artifactId>
464
+ <packaging>pom</packaging>
465
+
466
+ <name>FC Module ${MODULE_TITLE} - Util</name>
467
+ <description>Parent POM for fc-module-${MODULE_NAME} util plugins</description>
468
+
469
+ <repositories>
470
+ <repository>
471
+ <id>maven-s3-public-repo</id>
472
+ <name>FluentCommerce Public Repo</name>
473
+ <url>https://public.maven.dev.fluentcommerce.com/releases</url>
474
+ </repository>
475
+ </repositories>
476
+
477
+ <pluginRepositories>
478
+ <pluginRepository>
479
+ <id>maven-s3-public-repo</id>
480
+ <name>FluentCommerce Public Repo</name>
481
+ <url>https://public.maven.dev.fluentcommerce.com/releases</url>
482
+ </pluginRepository>
483
+ </pluginRepositories>
484
+
485
+ <modules>
486
+ <module>${MODULE_ALIAS}</module>
487
+ </modules>
488
+
489
+ </project>
490
+ ```
491
+
492
+ ### Util Implementation POM (`plugins/util/<module-alias>/pom.xml`)
493
+
494
+ ```xml
495
+ <?xml version="1.0" encoding="UTF-8"?>
496
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
497
+ xmlns="http://maven.apache.org/POM/4.0.0"
498
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
499
+ <modelVersion>4.0.0</modelVersion>
500
+
501
+ <parent>
502
+ <groupId>${GROUP_ID}</groupId>
503
+ <artifactId>fc-module-${MODULE_NAME}-util</artifactId>
504
+ <version>${INITIAL_VERSION}</version>
505
+ </parent>
506
+
507
+ <artifactId>fc-util-${MODULE_NAME}</artifactId>
508
+
509
+ <name>FC Module ${MODULE_TITLE} - Utilities Implementation</name>
510
+ <description>Utilities and helper classes for ${MODULE_TITLE} module</description>
511
+
512
+ <properties>
513
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
514
+ <rubix-plugin-base.version>1.2.0.679</rubix-plugin-base.version>
515
+ <fluent-api-client.version>1.2.0.118</fluent-api-client.version>
516
+ <fluent-apollo-client-plugin.version>1.0.0.15</fluent-apollo-client-plugin.version>
517
+ <apollo.graphql.version>0.4.2</apollo.graphql.version>
518
+ <lombok.version>1.18.30</lombok.version>
519
+ <commons-lang3.version>3.4</commons-lang3.version>
520
+ <jackson.version>2.10.0</jackson.version>
521
+ <google.guava.version>19.0</google.guava.version>
522
+ <commons.collections4.version>4.3</commons.collections4.version>
523
+ </properties>
524
+
525
+ <dependencies>
526
+ <!-- Module Dependencies -->
527
+ <dependency>
528
+ <groupId>${GROUP_ID}</groupId>
529
+ <artifactId>fc-types-${MODULE_NAME}</artifactId>
530
+ <version>${project.version}</version>
531
+ </dependency>
532
+
533
+ <!-- Fluent Commerce dependencies -->
534
+ <dependency>
535
+ <groupId>com.fluentcommerce</groupId>
536
+ <artifactId>rubix-plugin-sdk</artifactId>
537
+ <version>${rubix-plugin-base.version}</version>
538
+ <scope>provided</scope>
539
+ </dependency>
540
+
541
+ <dependency>
542
+ <groupId>com.fluentcommerce</groupId>
543
+ <artifactId>fluent-api-client</artifactId>
544
+ <version>${fluent-api-client.version}</version>
545
+ <scope>provided</scope>
546
+ </dependency>
547
+
548
+ <!-- 3rd party libraries -->
549
+ <dependency>
550
+ <groupId>org.projectlombok</groupId>
551
+ <artifactId>lombok</artifactId>
552
+ <version>${lombok.version}</version>
553
+ <scope>provided</scope>
554
+ </dependency>
555
+
556
+ <dependency>
557
+ <groupId>org.apache.commons</groupId>
558
+ <artifactId>commons-lang3</artifactId>
559
+ <version>${commons-lang3.version}</version>
560
+ <scope>provided</scope>
561
+ </dependency>
562
+
563
+ <dependency>
564
+ <groupId>com.google.guava</groupId>
565
+ <artifactId>guava</artifactId>
566
+ <version>${google.guava.version}</version>
567
+ <scope>provided</scope>
568
+ </dependency>
569
+
570
+ <dependency>
571
+ <groupId>com.fasterxml.jackson.core</groupId>
572
+ <artifactId>jackson-core</artifactId>
573
+ <version>${jackson.version}</version>
574
+ </dependency>
575
+
576
+ <dependency>
577
+ <groupId>com.fasterxml.jackson.core</groupId>
578
+ <artifactId>jackson-databind</artifactId>
579
+ <version>${jackson.version}</version>
580
+ </dependency>
581
+
582
+ <dependency>
583
+ <groupId>org.apache.commons</groupId>
584
+ <artifactId>commons-collections4</artifactId>
585
+ <version>${commons.collections4.version}</version>
586
+ </dependency>
587
+
588
+ <!-- Apollo GraphQL -->
589
+ <dependency>
590
+ <groupId>com.apollographql.apollo</groupId>
591
+ <artifactId>apollo-runtime</artifactId>
592
+ <version>${apollo.graphql.version}</version>
593
+ </dependency>
594
+
595
+ <dependency>
596
+ <groupId>com.fasterxml.jackson.module</groupId>
597
+ <artifactId>jackson-module-jsonSchema</artifactId>
598
+ <version>${jackson.version}</version>
599
+ </dependency>
600
+ </dependencies>
601
+
602
+ <build>
603
+ <plugins>
604
+ <plugin>
605
+ <groupId>com.fluentcommerce</groupId>
606
+ <artifactId>apollo-client-maven-plugin</artifactId>
607
+ </plugin>
608
+ <plugin>
609
+ <groupId>org.apache.maven.plugins</groupId>
610
+ <artifactId>maven-compiler-plugin</artifactId>
611
+ <version>3.11.0</version>
612
+ <configuration>
613
+ <target>1.8</target>
614
+ <source>1.8</source>
615
+ <fork>true</fork>
616
+ <compilerArgs>
617
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
618
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
619
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
620
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
621
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
622
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
623
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
624
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
625
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
626
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
627
+ </compilerArgs>
628
+ <annotationProcessorPaths>
629
+ <path>
630
+ <groupId>org.projectlombok</groupId>
631
+ <artifactId>lombok</artifactId>
632
+ <version>${lombok.version}</version>
633
+ </path>
634
+ </annotationProcessorPaths>
635
+ </configuration>
636
+ </plugin>
637
+ </plugins>
638
+ </build>
639
+ </project>
640
+ ```
641
+
642
+ ### Rules Aggregator POM (`plugins/rules/pom.xml`)
643
+
644
+ ```xml
645
+ <?xml version="1.0" encoding="UTF-8"?>
646
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
647
+ xmlns="http://maven.apache.org/POM/4.0.0"
648
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
649
+ <modelVersion>4.0.0</modelVersion>
650
+
651
+ <parent>
652
+ <groupId>${GROUP_ID}</groupId>
653
+ <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
654
+ <version>${INITIAL_VERSION}</version>
655
+ </parent>
656
+
657
+ <artifactId>fc-module-${MODULE_NAME}-rules</artifactId>
658
+ <packaging>pom</packaging>
659
+
660
+ <name>FC Module ${MODULE_TITLE} - Rules</name>
661
+ <description>Parent POM for fc-module-${MODULE_NAME} rules plugins</description>
662
+
663
+ <modules>
664
+ <module>${MODULE_ALIAS}</module>
665
+ </modules>
666
+
667
+ </project>
668
+ ```
669
+
670
+ ### Rules Implementation POM (`plugins/rules/<module-alias>/pom.xml`)
671
+
672
+ This is the most important POM -- it produces the deployable OSGi bundle JAR.
673
+
674
+ ```xml
675
+ <?xml version="1.0" encoding="UTF-8"?>
676
+ <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
677
+ xmlns="http://maven.apache.org/POM/4.0.0"
678
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
679
+
680
+ <modelVersion>4.0.0</modelVersion>
681
+
682
+ <parent>
683
+ <groupId>${GROUP_ID}</groupId>
684
+ <artifactId>fc-module-${MODULE_NAME}-rules</artifactId>
685
+ <version>${INITIAL_VERSION}</version>
686
+ </parent>
687
+
688
+ <artifactId>fc-module-${MODULE_NAME}</artifactId>
689
+
690
+ <pluginRepositories>
691
+ <pluginRepository>
692
+ <id>maven-s3-public-repo</id>
693
+ <name>FluentCommerce Public Repo</name>
694
+ <url>https://public.maven.dev.fluentcommerce.com/releases</url>
695
+ </pluginRepository>
696
+ </pluginRepositories>
697
+
698
+ <repositories>
699
+ <repository>
700
+ <id>maven-s3-public-repo</id>
701
+ <name>FluentCommerce Public Repo</name>
702
+ <url>https://public.maven.dev.fluentcommerce.com/releases</url>
703
+ </repository>
704
+ </repositories>
705
+
706
+ <properties>
707
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
708
+ <rubix.artifact>${project.groupId}.${project.artifactId}</rubix.artifact>
709
+ <rubix.artifact.vendor>Fluent Commerce</rubix.artifact.vendor>
710
+ <rubix.osgi.symbolic.name>${OSGI_SYMBOLIC_NAME}</rubix.osgi.symbolic.name>
711
+ <rubix.osgi.activator/>
712
+ <rubix.osgi.export.package/>
713
+ <rubix.osgi.export.service/>
714
+ <rubix-plugin-base.version>1.2.0.679</rubix-plugin-base.version>
715
+ <fluent-api-client.version>1.2.0.118</fluent-api-client.version>
716
+ <fluent-apollo-client-plugin.version>1.0.0.15</fluent-apollo-client-plugin.version>
717
+ <slf4j.version>1.7.22</slf4j.version>
718
+
719
+ <!-- 3rd party dependency versions -->
720
+ <apollo.graphql.version>0.4.2</apollo.graphql.version>
721
+ <commons.collections4.version>4.3</commons.collections4.version>
722
+ <lombok.version>1.18.30</lombok.version>
723
+ <commons-lang3.version>3.4</commons-lang3.version>
724
+ <jackson.version>2.10.0</jackson.version>
725
+ <google.guava.version>19.0</google.guava.version>
726
+ <okhttp.version>3.14.9</okhttp.version>
727
+
728
+ <!-- test dependency versions -->
729
+ <junit-jupiter.version>5.11.0</junit-jupiter.version>
730
+ <mockito.version>4.11.0</mockito.version>
731
+
732
+ <!-- maven plugin versions -->
733
+ <lombok.maven.plugin.version>1.16.8.0</lombok.maven.plugin.version>
734
+ <maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
735
+ <maven.dependency.plugin.version>2.8</maven.dependency.plugin.version>
736
+ <maven.resources.plugin.version>2.6</maven.resources.plugin.version>
737
+ <maven.bundle.plugin.version>5.1.9</maven.bundle.plugin.version>
738
+ </properties>
739
+
740
+ <dependencies>
741
+
742
+ <!-- Module Dependencies -->
743
+ <dependency>
744
+ <groupId>${GROUP_ID}</groupId>
745
+ <artifactId>fc-types-${MODULE_NAME}</artifactId>
746
+ <version>${project.version}</version>
747
+ </dependency>
748
+ <dependency>
749
+ <groupId>${GROUP_ID}</groupId>
750
+ <artifactId>fc-util-${MODULE_NAME}</artifactId>
751
+ <version>${project.version}</version>
752
+ </dependency>
753
+
754
+ <!-- INCLUDED DEPENDENCIES -->
755
+ <dependency>
756
+ <groupId>com.fasterxml.jackson.module</groupId>
757
+ <artifactId>jackson-module-jsonSchema</artifactId>
758
+ <version>${jackson.version}</version>
759
+ </dependency>
760
+
761
+ <!-- PROVIDED DEPENDENCIES -->
762
+ <dependency>
763
+ <groupId>com.fluentcommerce</groupId>
764
+ <artifactId>rubix-plugin-sdk</artifactId>
765
+ <version>${rubix-plugin-base.version}</version>
766
+ <scope>provided</scope>
767
+ </dependency>
768
+
769
+ <dependency>
770
+ <groupId>org.slf4j</groupId>
771
+ <artifactId>slf4j-api</artifactId>
772
+ <version>${slf4j.version}</version>
773
+ <scope>provided</scope>
774
+ </dependency>
775
+
776
+ <dependency>
777
+ <groupId>org.slf4j</groupId>
778
+ <artifactId>slf4j-simple</artifactId>
779
+ <version>${slf4j.version}</version>
780
+ <scope>provided</scope>
781
+ </dependency>
782
+
783
+ <dependency>
784
+ <groupId>org.projectlombok</groupId>
785
+ <artifactId>lombok</artifactId>
786
+ <version>${lombok.version}</version>
787
+ <scope>provided</scope>
788
+ </dependency>
789
+
790
+ <dependency>
791
+ <groupId>org.apache.commons</groupId>
792
+ <artifactId>commons-lang3</artifactId>
793
+ <version>${commons-lang3.version}</version>
794
+ <scope>provided</scope>
795
+ </dependency>
796
+
797
+ <dependency>
798
+ <groupId>com.fluentcommerce</groupId>
799
+ <artifactId>fluent-api-client</artifactId>
800
+ <version>${fluent-api-client.version}</version>
801
+ <scope>provided</scope>
802
+ </dependency>
803
+
804
+ <dependency>
805
+ <groupId>com.google.guava</groupId>
806
+ <artifactId>guava</artifactId>
807
+ <version>${google.guava.version}</version>
808
+ <scope>provided</scope>
809
+ </dependency>
810
+
811
+ <dependency>
812
+ <groupId>com.apollographql.apollo</groupId>
813
+ <artifactId>apollo-api</artifactId>
814
+ <version>${apollo.graphql.version}</version>
815
+ <scope>provided</scope>
816
+ </dependency>
817
+
818
+ <!-- 3rd party libs -->
819
+ <dependency>
820
+ <groupId>com.fasterxml.jackson.core</groupId>
821
+ <artifactId>jackson-core</artifactId>
822
+ <version>${jackson.version}</version>
823
+ </dependency>
824
+
825
+ <dependency>
826
+ <groupId>com.fasterxml.jackson.core</groupId>
827
+ <artifactId>jackson-databind</artifactId>
828
+ <version>${jackson.version}</version>
829
+ </dependency>
830
+
831
+ <dependency>
832
+ <groupId>org.apache.commons</groupId>
833
+ <artifactId>commons-collections4</artifactId>
834
+ <version>${commons.collections4.version}</version>
835
+ </dependency>
836
+
837
+ <!-- TEST -->
838
+ <dependency>
839
+ <groupId>com.squareup.okio</groupId>
840
+ <artifactId>okio</artifactId>
841
+ <version>1.17.6</version>
842
+ <scope>test</scope>
843
+ </dependency>
844
+
845
+ <dependency>
846
+ <groupId>org.mockito</groupId>
847
+ <artifactId>mockito-core</artifactId>
848
+ <version>${mockito.version}</version>
849
+ <scope>test</scope>
850
+ </dependency>
851
+
852
+ <dependency>
853
+ <groupId>org.mockito</groupId>
854
+ <artifactId>mockito-inline</artifactId>
855
+ <version>${mockito.version}</version>
856
+ <scope>test</scope>
857
+ </dependency>
858
+
859
+ <dependency>
860
+ <groupId>org.mockito</groupId>
861
+ <artifactId>mockito-junit-jupiter</artifactId>
862
+ <version>${mockito.version}</version>
863
+ <scope>test</scope>
864
+ </dependency>
865
+
866
+ <dependency>
867
+ <groupId>com.fluentcommerce</groupId>
868
+ <artifactId>rubix-test-mockery</artifactId>
869
+ <version>${rubix-plugin-base.version}</version>
870
+ <scope>test</scope>
871
+ </dependency>
872
+
873
+ <dependency>
874
+ <groupId>org.junit.jupiter</groupId>
875
+ <artifactId>junit-jupiter</artifactId>
876
+ <version>${junit-jupiter.version}</version>
877
+ <scope>test</scope>
878
+ </dependency>
879
+
880
+ <dependency>
881
+ <groupId>org.junit.jupiter</groupId>
882
+ <artifactId>junit-jupiter-api</artifactId>
883
+ <version>${junit-jupiter.version}</version>
884
+ <scope>test</scope>
885
+ </dependency>
886
+
887
+ <dependency>
888
+ <groupId>org.junit.jupiter</groupId>
889
+ <artifactId>junit-jupiter-engine</artifactId>
890
+ <version>${junit-jupiter.version}</version>
891
+ <scope>test</scope>
892
+ </dependency>
893
+
894
+ </dependencies>
895
+
896
+ <build>
897
+ <plugins>
898
+ <plugin>
899
+ <groupId>org.projectlombok</groupId>
900
+ <artifactId>lombok-maven-plugin</artifactId>
901
+ <version>${lombok.maven.plugin.version}</version>
902
+ <executions>
903
+ <execution>
904
+ <phase>generate-sources</phase>
905
+ <goals>
906
+ <goal>delombok</goal>
907
+ </goals>
908
+ </execution>
909
+ </executions>
910
+ </plugin>
911
+ <plugin>
912
+ <groupId>org.apache.maven.plugins</groupId>
913
+ <artifactId>maven-compiler-plugin</artifactId>
914
+ <version>${maven.compiler.plugin.version}</version>
915
+ <configuration>
916
+ <target>1.8</target>
917
+ <source>1.8</source>
918
+ <fork>true</fork>
919
+ <compilerArgs>
920
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
921
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
922
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
923
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
924
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
925
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
926
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
927
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
928
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
929
+ <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
930
+ </compilerArgs>
931
+ <annotationProcessorPaths>
932
+ <path>
933
+ <groupId>org.projectlombok</groupId>
934
+ <artifactId>lombok</artifactId>
935
+ <version>${lombok.version}</version>
936
+ </path>
937
+ </annotationProcessorPaths>
938
+ </configuration>
939
+ </plugin>
940
+ <plugin>
941
+ <groupId>org.apache.maven.plugins</groupId>
942
+ <artifactId>maven-dependency-plugin</artifactId>
943
+ <version>${maven.dependency.plugin.version}</version>
944
+ </plugin>
945
+ <plugin>
946
+ <groupId>org.apache.maven.plugins</groupId>
947
+ <artifactId>maven-resources-plugin</artifactId>
948
+ <version>${maven.resources.plugin.version}</version>
949
+ </plugin>
950
+ <plugin>
951
+ <groupId>org.apache.felix</groupId>
952
+ <artifactId>maven-bundle-plugin</artifactId>
953
+ <version>${maven.bundle.plugin.version}</version>
954
+ <extensions>true</extensions>
955
+ <executions>
956
+ <execution>
957
+ <phase>generate-sources</phase>
958
+ <goals>
959
+ <goal>cleanVersions</goal>
960
+ </goals>
961
+ </execution>
962
+ <execution>
963
+ <id>bundle-manifest</id>
964
+ <phase>process-classes</phase>
965
+ <goals>
966
+ <goal>manifest</goal>
967
+ </goals>
968
+ </execution>
969
+ <execution>
970
+ <id>package-bundle</id>
971
+ <phase>package</phase>
972
+ <goals>
973
+ <goal>bundle</goal>
974
+ </goals>
975
+ </execution>
976
+ </executions>
977
+ <configuration>
978
+ <instructions>
979
+ <Bundle-Name>${project.name}</Bundle-Name>
980
+ <Bundle-SymbolicName>${rubix.osgi.symbolic.name}</Bundle-SymbolicName>
981
+ <Bundle-Activator>${rubix.osgi.activator}</Bundle-Activator>
982
+ <Bundle-Vendor>${rubix.artifact.vendor}</Bundle-Vendor>
983
+ <Bundle-Version>${project.version}</Bundle-Version>
984
+ <Export-Package>${rubix.osgi.export.package}</Export-Package>
985
+ <Export-Service>${rubix.osgi.export.service}</Export-Service>
986
+ <Import-Package>!android.*,!org.conscrypt,!org.openjsse.*,!okhttp3.*,!okio.*,*</Import-Package>
987
+ <Embed-Dependency>*;scope=compile|runtime;inline=true</Embed-Dependency>
988
+ <Bundle-ClassPath>.,{maven-dependencies}</Bundle-ClassPath>
989
+ <Embed-Transitive>true</Embed-Transitive>
990
+ <Rubix-Rules>$(classes;CONCRETE;ANNOTATION;com.fluentretail.rubix.rule.meta.RuleInfo)</Rubix-Rules>
991
+ <Rubix-Account>_</Rubix-Account>
992
+ </instructions>
993
+ </configuration>
994
+ </plugin>
995
+ </plugins>
996
+ </build>
997
+ </project>
998
+ ```
999
+
1000
+ ### Module Manifest (`resources/module.json`)
1001
+
1002
+ ```json
1003
+ {
1004
+ "_schema": "1.0.0",
1005
+ "name": "fluent-commerce/fc-module-${MODULE_NAME}",
1006
+ "version": "${INITIAL_VERSION}",
1007
+ "title": "Fluent Commerce - ${MODULE_TITLE} Module",
1008
+ "description": "${MODULE_TITLE} extension module providing custom rules and utilities for the Rubix platform",
1009
+ "authors": [
1010
+ {
1011
+ "name": "Fluent Commerce",
1012
+ "email": "support@fluentcommerce.com",
1013
+ "web": "https://fluentcommerce.com"
1014
+ }
1015
+ ],
1016
+ "dependencies": [
1017
+ {
1018
+ "name": "fluent-commerce/core",
1019
+ "type": "module",
1020
+ "version": "2.x.x"
1021
+ }
1022
+ ],
1023
+ "contracts": [],
1024
+ "rules": [
1025
+ "${RULE_NAME_1}",
1026
+ "${RULE_NAME_2}"
1027
+ ],
1028
+ "compatibility": {
1029
+ "minRubixVersion": "1.2.0",
1030
+ "rubixPluginSdk": "1.2.0.679",
1031
+ "fluentApiClient": "1.2.0.118"
1032
+ }
1033
+ }
1034
+ ```
1035
+
1036
+ **Important:** If no `--rules` are provided, the `"rules"` array must be empty `[]`. Each entry is a simple string matching the `@RuleInfo(name = "...")` annotation value. Do NOT include package prefixes or account prefixes in this array.
1037
+
1038
+ ### Config Prefix System
1039
+
1040
+ The Fluent CLI uses a prefix system in `module.config.json` to scope variable values to specific contexts. Understanding this system is essential for extension modules that include workflows and settings.
1041
+
1042
+ #### How Config Prefixes Work
1043
+
1044
+ Config keys follow the pattern: `<prefix>:<variable.name>`
1045
+
1046
+ The CLI filters keys based on the asset context being processed and selects the most specific match.
1047
+
1048
+ #### Available Prefixes
1049
+
1050
+ | Prefix | Applies To | Specificity | Example |
1051
+ |--------|-----------|-------------|---------|
1052
+ | `default:` | All assets (workflows, settings, data) | 0 (lowest, fallback) | `"default:network.ref": "NET_DEFAULT"` |
1053
+ | `workflow:` | Workflows only | 0 | `"workflow:notification.enabled": "true"` |
1054
+ | `workflow:<type>:` | Specific entity type workflows | 1 | `"workflow:order:network.ref": "NET_ORDER"` |
1055
+ | `workflow:<type>:<subtype>:` | Specific entity type + subtype | 2 (highest) | `"workflow:order:hd:carrier.ref": "HD_CARRIER"` |
1056
+ | `setting:` | Settings only | 0 | `"setting:api.timeout": "30000"` |
1057
+
1058
+ **Specificity rules:** More colons after the prefix = higher specificity = wins. For a workflow `ORDER::HD`:
1059
+ - `workflow:order:hd:network.ref` (2 levels) beats
1060
+ - `workflow:order:network.ref` (1 level) beats
1061
+ - `default:network.ref` (0 levels)
1062
+
1063
+ #### Context-Based Filtering
1064
+
1065
+ | Asset Folder | Recognized Prefixes |
1066
+ |-------------|---------------------|
1067
+ | `assets/workflows/` | `default:`, `workflow:`, `workflow:<type>:`, `workflow:<type>:<subtype>:` |
1068
+ | `assets/settings/` | `default:`, `setting:` |
1069
+ | All other asset folders | `default:` only |
1070
+
1071
+ #### Built-in Auto-Injected Variables
1072
+
1073
+ These variables are available in all asset files without defining them in `module.config.json`:
1074
+ - `account.id` -- Fluent account ID
1075
+ - `retailer.id` -- Target retailer numeric ID
1076
+ - `retailer.ref` -- Target retailer reference string
1077
+ - `retailer.name` -- Target retailer name
1078
+
1079
+ Note: `account.host` is NOT auto-injected. If needed, define it explicitly: `"default:account.host": "acme.sandbox.api.fluentretail.com"`.
1080
+
1081
+ #### Variable Syntax
1082
+
1083
+ Use `[[variable]]` tokens in any JSON or CSV asset file:
1084
+
1085
+ ```json
1086
+ {
1087
+ "ref": "NET_HD_[[retailer.id]]",
1088
+ "networkRef": "[[network.ref]]",
1089
+ "retailerId": [[retailer.id]]
1090
+ }
1091
+ ```
1092
+
1093
+ **Important:** Unresolved `[[tokens]]` are left as-is -- the CLI does not error on missing substitutions. Always scan for unreplaced tokens before deploying:
1094
+
1095
+ ```bash
1096
+ rg '\[\[.*\]\]' resources/assets/
1097
+ ```
1098
+
1099
+ #### Example module.config.json
1100
+
1101
+ When the scaffold creates `resources/module.config.json`, it generates an empty object `{}`. Populate it with prefixed keys as needed:
1102
+
1103
+ ```json
1104
+ {
1105
+ "default:carrier.ref": "CARRIER_STD",
1106
+ "default:network.ref": "NET_DEFAULT",
1107
+ "workflow:order:hd:network.ref": "NET_HD",
1108
+ "workflow:order:hd:carrier.ref": "HD_CARRIER",
1109
+ "workflow:order:cc:network.ref": "NET_CC",
1110
+ "setting:webhook.order.url": "https://example.com/webhook/order"
1111
+ }
1112
+ ```
1113
+
1114
+ The build scripts (`scripts/build-module.sh` and `scripts/build-module.ps1`) already handle copying `module.config.json` into the distribution ZIP alongside `module.json`. When deploying with `fluent module install`, the CLI reads this file and substitutes `[[variable]]` tokens in all asset files before uploading.
1115
+
1116
+ ### Rule Class Template
1117
+
1118
+ ```java
1119
+ package com.fluentcommerce.rule.${PACKAGE};
1120
+
1121
+ import com.fluentcommerce.common.BaseRule;
1122
+ import com.fluentcommerce.common.ContextWrapper;
1123
+ import com.fluentretail.rubix.rule.meta.ParamString;
1124
+ import com.fluentretail.rubix.rule.meta.RuleInfo;
1125
+ import lombok.extern.slf4j.Slf4j;
1126
+
1127
+ /**
1128
+ * ${RULE_DESCRIPTION}
1129
+ */
1130
+ @RuleInfo(
1131
+ name = "${RULE_NAME}",
1132
+ description = "${RULE_DESCRIPTION}"
1133
+ )
1134
+ @Slf4j
1135
+ public class ${RULE_NAME} extends BaseRule {
1136
+
1137
+ private static final String CLASS_NAME = ${RULE_NAME}.class.getSimpleName();
1138
+
1139
+ @Override
1140
+ public void run(ContextWrapper context) {
1141
+ String accountId = context.getEvent().getAccountId();
1142
+
1143
+ log.info("[{}] [{}] - Processing event: {}", accountId, CLASS_NAME,
1144
+ context.getEvent().getName());
1145
+ context.addLog("Processing " + CLASS_NAME);
1146
+
1147
+ try {
1148
+ // === Rule logic here ===
1149
+
1150
+ context.addLog(CLASS_NAME + " completed successfully");
1151
+ } catch (Exception e) {
1152
+ log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
1153
+ context.addLog("Error in " + CLASS_NAME + ": " + e.getMessage());
1154
+ }
1155
+ }
1156
+ }
1157
+ ```
1158
+
1159
+ **Fallback rule class (when no BaseRule/ContextWrapper exists in the module):**
1160
+
1161
+ If this is a brand-new module without an existing `BaseRule` class, use the direct Rubix SDK API instead:
1162
+
1163
+ ```java
1164
+ package com.fluentcommerce.rule.${PACKAGE};
1165
+
1166
+ import com.fluentretail.rubix.event.Event;
1167
+ import com.fluentretail.rubix.rule.meta.RuleInfo;
1168
+ import com.fluentretail.rubix.v2.context.Context;
1169
+ import com.fluentretail.rubix.v2.rule.Rule;
1170
+ import lombok.extern.slf4j.Slf4j;
1171
+
1172
+ /**
1173
+ * ${RULE_DESCRIPTION}
1174
+ */
1175
+ @RuleInfo(
1176
+ name = "${RULE_NAME}",
1177
+ description = "${RULE_DESCRIPTION}"
1178
+ )
1179
+ @Slf4j
1180
+ public class ${RULE_NAME} implements Rule {
1181
+
1182
+ private static final String CLASS_NAME = ${RULE_NAME}.class.getSimpleName();
1183
+
1184
+ @Override
1185
+ public void run(Context context) {
1186
+ Event event = context.getEvent();
1187
+ String accountId = event.getAccountId();
1188
+
1189
+ log.info("[{}] [{}] - Processing event: {} for entity {}",
1190
+ accountId, CLASS_NAME, event.getName(), event.getEntityRef());
1191
+
1192
+ try {
1193
+ // === Rule logic here ===
1194
+
1195
+ log.info("[{}] [{}] - Completed successfully", accountId, CLASS_NAME);
1196
+ } catch (Exception e) {
1197
+ log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
1198
+ }
1199
+ }
1200
+ }
1201
+ ```
1202
+
1203
+ **Which template to use:** For new modules with no shared `BaseRule`, use the fallback (`implements Rule`). For modules that already have a `BaseRule` and `ContextWrapper` in their `common` package (or in a util dependency), use the primary template (`extends BaseRule`). The scaffolder should check for the existence of `BaseRule.java` under `plugins/util/` or `plugins/rules/` and choose accordingly. Default for greenfield: use the fallback.
1204
+
1205
+ ### Test Class Template
1206
+
1207
+ ```java
1208
+ package com.fluentcommerce.rule.${PACKAGE};
1209
+
1210
+ import com.fluentretail.rubix.event.Event;
1211
+ import com.fluentretail.rubix.rule.meta.RuleInfo;
1212
+ import com.fluentretail.rubix.v2.context.Context;
1213
+ import com.fluentretail.rubix.v2.action.Action;
1214
+ import com.fluentretail.rubix.v2.action.MutationAction;
1215
+ import com.fluentretail.rubix.v2.action.EventAction;
1216
+ import org.junit.jupiter.api.BeforeEach;
1217
+ import org.junit.jupiter.api.Test;
1218
+ import org.junit.jupiter.api.DisplayName;
1219
+ import org.mockito.Mock;
1220
+ import org.mockito.MockitoAnnotations;
1221
+
1222
+ import java.util.HashMap;
1223
+ import java.util.Map;
1224
+
1225
+ import static org.junit.jupiter.api.Assertions.*;
1226
+ import static org.mockito.Mockito.*;
1227
+
1228
+ class ${RULE_NAME}Test {
1229
+
1230
+ private ${RULE_NAME} rule;
1231
+
1232
+ @Mock private Context context;
1233
+ @Mock private Event event;
1234
+ @Mock private Action action;
1235
+ @Mock private MutationAction mutationAction;
1236
+ @Mock private EventAction eventAction;
1237
+
1238
+ @BeforeEach
1239
+ void setUp() {
1240
+ MockitoAnnotations.openMocks(this);
1241
+ rule = new ${RULE_NAME}();
1242
+
1243
+ when(context.getEvent()).thenReturn(event);
1244
+ when(context.action()).thenReturn(action);
1245
+ when(action.mutation()).thenReturn(mutationAction);
1246
+ when(action.eventAction()).thenReturn(eventAction);
1247
+ when(event.getAccountId()).thenReturn("TEST_ACCOUNT");
1248
+ when(event.getName()).thenReturn("TestEvent");
1249
+ when(event.getEntityRef()).thenReturn("TEST-001");
1250
+ }
1251
+
1252
+ @Test
1253
+ @DisplayName("Rule has correct @RuleInfo annotation")
1254
+ void hasRuleInfo() {
1255
+ RuleInfo info = ${RULE_NAME}.class.getAnnotation(RuleInfo.class);
1256
+ assertNotNull(info);
1257
+ assertEquals("${RULE_NAME}", info.name());
1258
+ }
1259
+
1260
+ @Test
1261
+ @DisplayName("Processes event successfully")
1262
+ void processesSuccessfully() {
1263
+ // Should not throw
1264
+ assertDoesNotThrow(() -> rule.run(context));
1265
+ }
1266
+
1267
+ @Test
1268
+ @DisplayName("Handles exception gracefully")
1269
+ void handlesExceptionGracefully() {
1270
+ when(event.getAccountId()).thenThrow(new RuntimeException("Simulated failure"));
1271
+
1272
+ // Rule should not propagate the exception
1273
+ assertDoesNotThrow(() -> rule.run(context));
1274
+ }
1275
+ }
1276
+ ```
1277
+
1278
+ ### Build Script - Bash (`scripts/build-module.sh`)
1279
+
1280
+ ```bash
1281
+ #!/bin/bash
1282
+ set -euo pipefail
1283
+
1284
+ VERBOSE=false
1285
+ BUILD_PLUGINS=true
1286
+ BUILD_MODULE=true
1287
+
1288
+ parse_options() {
1289
+ while getopts "mpx" opt; do
1290
+ case $opt in
1291
+ m) BUILD_MODULE=false ;;
1292
+ p) BUILD_PLUGINS=false ;;
1293
+ x) VERBOSE=true ;;
1294
+ *)
1295
+ print_usage
1296
+ exit 1
1297
+ ;;
1298
+ esac
1299
+ done
1300
+ }
1301
+
1302
+ print_usage() {
1303
+ echo "Usage: $0 [-m] [-p] [-x]"
1304
+ echo " -m Skip module zip creation"
1305
+ echo " -p Skip plugin build and package existing artifacts"
1306
+ echo " -x Enable verbose mode"
1307
+ }
1308
+
1309
+ log() {
1310
+ if [ "$VERBOSE" = true ]; then
1311
+ echo "$@"
1312
+ fi
1313
+ }
1314
+
1315
+ require_command() {
1316
+ local command_name=$1
1317
+ if ! command -v "$command_name" >/dev/null 2>&1; then
1318
+ echo "Error: required command '$command_name' was not found in PATH"
1319
+ exit 1
1320
+ fi
1321
+ }
1322
+
1323
+ clean_folder() {
1324
+ local dist_dir=$1
1325
+ if [ -d "$dist_dir" ]; then
1326
+ log "Directory $dist_dir exist, clearing..."
1327
+ rm -rf "${dist_dir:?}/"
1328
+ fi
1329
+ }
1330
+
1331
+ copy_assets() {
1332
+ local resources_dir=$1
1333
+ local dist_dir=$2
1334
+ local dist_assets_dir=$3
1335
+
1336
+ log "Resources Dir: '$resources_dir', Destination Dir: '$dist_dir'"
1337
+
1338
+ if [ ! -d "$resources_dir" ]; then
1339
+ echo "Error: Resources directory '$resources_dir' does not exist"
1340
+ exit 1
1341
+ fi
1342
+
1343
+ if [ -e "$resources_dir/module.json" ]; then
1344
+ cp "$resources_dir/module.json" "$dist_dir/"
1345
+ else
1346
+ log "No module.json found, skipping."
1347
+ fi
1348
+
1349
+ if [ -e "$resources_dir/module.config.json" ]; then
1350
+ cp "$resources_dir/module.config.json" "$dist_dir/"
1351
+ else
1352
+ log "No module.config.json found, skipping."
1353
+ fi
1354
+
1355
+ mkdir -p "$dist_assets_dir"
1356
+ for entry in "$resources_dir"/*; do
1357
+ [ -e "$entry" ] || continue
1358
+ entry_name=$(basename "$entry")
1359
+ case "$entry_name" in
1360
+ module.json|module.config.json|README.md|CHANGELOG.md|LICENSE.md|workflows-backup)
1361
+ log "Skipping non-asset entry: '$entry_name'"
1362
+ continue
1363
+ ;;
1364
+ esac
1365
+ cp -r "$entry" "$dist_assets_dir/"
1366
+ done
1367
+ }
1368
+
1369
+ copy_jar_files() {
1370
+ local plugin_folder=$1
1371
+ local destination=$2
1372
+
1373
+ if [ -d "$plugin_folder" ]; then
1374
+ log "Plugin folder exists, copying plugin files from '$plugin_folder' to '$destination'"
1375
+ mkdir -p "$destination"
1376
+ else
1377
+ log "No plugins to copy from: '$plugin_folder'"
1378
+ return 0
1379
+ fi
1380
+
1381
+ for TARGET_FOLDER in "$plugin_folder"/*/target; do
1382
+ log "TARGET_FOLDER: '$TARGET_FOLDER'"
1383
+ if [ -d "$TARGET_FOLDER" ]; then
1384
+ for JAR_FILE in "$TARGET_FOLDER"/*.jar; do
1385
+ log "JAR FILE: '$JAR_FILE'"
1386
+ if [ -f "$JAR_FILE" ]; then
1387
+ echo "Copying $JAR_FILE to $destination"
1388
+ cp "$JAR_FILE" "$destination"
1389
+ fi
1390
+ done
1391
+ fi
1392
+ done
1393
+ }
1394
+
1395
+ zip_module() {
1396
+ local dist_dir=$1
1397
+ local module_name=$2
1398
+
1399
+ if [ -f "$dist_dir/$module_name.zip" ]; then
1400
+ rm "$dist_dir/$module_name.zip"
1401
+ fi
1402
+
1403
+ local zip_file="$module_name.zip"
1404
+ (cd "$dist_dir/$module_name" && zip -r "$zip_file" . -q)
1405
+ mv "$dist_dir/$module_name/$zip_file" "$dist_dir"
1406
+
1407
+ log "Contents of '$dist_dir' zipped into '$zip_file'."
1408
+ }
1409
+
1410
+ main() {
1411
+ parse_options "$@"
1412
+ require_command sed
1413
+ require_command cp
1414
+ require_command basename
1415
+ if [ "$BUILD_PLUGINS" = true ]; then
1416
+ require_command mvn
1417
+ fi
1418
+ if [ "$BUILD_MODULE" = true ]; then
1419
+ require_command zip
1420
+ fi
1421
+
1422
+ SCRIPT_DIR=$(dirname "$(realpath "$0")")
1423
+ MODULE_BASE_DIR="$SCRIPT_DIR/.."
1424
+ PLUGIN_DIR="$SCRIPT_DIR/../plugins"
1425
+ PLUGIN_RULES_DIR="$PLUGIN_DIR/rules"
1426
+ DIST_DIR="$MODULE_BASE_DIR/dist"
1427
+ RESOURCES_DIR="$MODULE_BASE_DIR/resources"
1428
+
1429
+ log "Script Directory: $SCRIPT_DIR"
1430
+ log "Module Directory: $MODULE_BASE_DIR"
1431
+
1432
+ if [ $BUILD_PLUGINS = true ]; then
1433
+ log "Building plugins..."
1434
+ cd "$PLUGIN_DIR" || exit 1
1435
+ mvn clean package || {
1436
+ echo "Maven build failed"
1437
+ exit 1
1438
+ }
1439
+ else
1440
+ log "Skipping building plugins..."
1441
+ fi
1442
+
1443
+ module_name=$(sed -n 's/.*"name": *"\([^"]*\)".*/\1/p' "$RESOURCES_DIR/module.json" | head -n 1)
1444
+ sanitized_name=$(echo "$module_name" | sed 's|/|-|g')
1445
+ module_version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$RESOURCES_DIR/module.json" | head -n 1)
1446
+ combined_name="$sanitized_name-$module_version"
1447
+ module_dist_dir="$DIST_DIR/$combined_name"
1448
+ module_dist_assets_dir="$module_dist_dir/assets"
1449
+ module_dist_plugins_dir="$module_dist_assets_dir/rules"
1450
+
1451
+ clean_folder "$DIST_DIR"
1452
+ mkdir -p "$module_dist_dir"
1453
+
1454
+ for f in CHANGELOG.md README.md LICENSE.md; do
1455
+ if [ -e "$MODULE_BASE_DIR/$f" ]; then
1456
+ cp "$MODULE_BASE_DIR/$f" "$module_dist_dir/"
1457
+ else
1458
+ log "No $f found, skipping."
1459
+ fi
1460
+ done
1461
+
1462
+ copy_assets "$RESOURCES_DIR" "$module_dist_dir" "$module_dist_assets_dir"
1463
+ copy_jar_files "$PLUGIN_RULES_DIR" "$module_dist_plugins_dir"
1464
+
1465
+ if [ $BUILD_MODULE = true ]; then
1466
+ log "Building module..."
1467
+ zip_module "$DIST_DIR" "$combined_name"
1468
+ else
1469
+ log "Skipping building module zip..."
1470
+ fi
1471
+ }
1472
+
1473
+ main "$@"
1474
+ ```
1475
+
1476
+ ### Build Script - PowerShell (`scripts/build-module.ps1`)
1477
+
1478
+ ```powershell
1479
+ #!/usr/bin/env pwsh
1480
+
1481
+ <#
1482
+ .SYNOPSIS
1483
+ Build module script for FluentCommerce modules
1484
+
1485
+ .DESCRIPTION
1486
+ This script builds rule plugins and packages the module assets into a distributable zip file.
1487
+
1488
+ .PARAMETER SkipModule
1489
+ Skip zipping the module
1490
+
1491
+ .PARAMETER SkipPlugins
1492
+ Skip building rule plugins
1493
+
1494
+ .PARAMETER VerboseLogging
1495
+ Enable verbose logging
1496
+
1497
+ .EXAMPLE
1498
+ .\build-module.ps1 -VerboseLogging
1499
+
1500
+ .EXAMPLE
1501
+ .\build-module.ps1 -SkipPlugins
1502
+ #>
1503
+
1504
+ [CmdletBinding()]
1505
+ param(
1506
+ [Alias('m')]
1507
+ [switch]$SkipModule,
1508
+
1509
+ [Alias('p')]
1510
+ [switch]$SkipPlugins,
1511
+
1512
+ [Alias('x')]
1513
+ [switch]$VerboseLogging
1514
+ )
1515
+
1516
+ $Script:BuildPlugins = -not $SkipPlugins
1517
+ $Script:BuildModule = -not $SkipModule
1518
+ $Script:VerboseMode = $VerboseLogging
1519
+
1520
+ function Write-Log {
1521
+ param([string]$Message)
1522
+ if ($Script:VerboseMode) {
1523
+ Write-Host $Message
1524
+ }
1525
+ }
1526
+
1527
+ function Remove-FolderIfExists {
1528
+ param([string]$Path)
1529
+ if (Test-Path $Path) {
1530
+ Write-Log "Directory $Path exists, clearing..."
1531
+ Remove-Item -Path $Path -Recurse -Force
1532
+ }
1533
+ }
1534
+
1535
+ function Copy-Assets {
1536
+ param(
1537
+ [string]$ResourcesDir,
1538
+ [string]$DistDir,
1539
+ [string]$DistAssetsDir
1540
+ )
1541
+ Write-Log "Resources Dir: '$ResourcesDir', Destination Dir: '$DistDir'"
1542
+
1543
+ if (-not (Test-Path $ResourcesDir)) {
1544
+ Write-Error "Resources directory '$ResourcesDir' does not exist"
1545
+ exit 1
1546
+ }
1547
+
1548
+ $moduleJsonPath = Join-Path $ResourcesDir "module.json"
1549
+ if (Test-Path $moduleJsonPath) {
1550
+ Copy-Item $moduleJsonPath $DistDir
1551
+ }
1552
+
1553
+ $moduleConfigPath = Join-Path $ResourcesDir "module.config.json"
1554
+ if (Test-Path $moduleConfigPath) {
1555
+ Copy-Item $moduleConfigPath $DistDir
1556
+ }
1557
+
1558
+ if (-not (Test-Path $DistAssetsDir)) {
1559
+ New-Item -ItemType Directory -Path $DistAssetsDir -Force | Out-Null
1560
+ }
1561
+
1562
+ $excludedEntries = @("module.json", "module.config.json", "README.md", "CHANGELOG.md", "LICENSE.md", "workflows-backup")
1563
+ Get-ChildItem -Path $ResourcesDir -Force | ForEach-Object {
1564
+ if ($excludedEntries -contains $_.Name) {
1565
+ Write-Log "Skipping non-asset entry: '$($_.Name)'"
1566
+ } else {
1567
+ Copy-Item -Path $_.FullName -Destination $DistAssetsDir -Recurse -Force
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ function Copy-JarFiles {
1573
+ param(
1574
+ [string]$PluginFolder,
1575
+ [string]$Destination
1576
+ )
1577
+ if (Test-Path $PluginFolder) {
1578
+ Write-Log "Plugin folder exists, copying from '$PluginFolder' to '$Destination'"
1579
+ New-Item -ItemType Directory -Path $Destination -Force | Out-Null
1580
+ } else {
1581
+ Write-Log "No plugins to copy from: '$PluginFolder'"
1582
+ return
1583
+ }
1584
+
1585
+ $targetFolders = Get-ChildItem -Path $PluginFolder -Directory | Where-Object { Test-Path (Join-Path $_.FullName "target") }
1586
+ foreach ($folder in $targetFolders) {
1587
+ $targetFolder = Join-Path $folder.FullName "target"
1588
+ if (Test-Path $targetFolder) {
1589
+ $jarFiles = Get-ChildItem -Path $targetFolder -Filter "*.jar"
1590
+ foreach ($jarFile in $jarFiles) {
1591
+ Write-Host "Copying $($jarFile.FullName) to $Destination"
1592
+ Copy-Item $jarFile.FullName $Destination
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ function New-ModuleZip {
1599
+ param(
1600
+ [string]$DistDir,
1601
+ [string]$ModuleName
1602
+ )
1603
+ $zipPath = Join-Path $DistDir "$ModuleName.zip"
1604
+ if (Test-Path $zipPath) {
1605
+ Remove-Item $zipPath -Force
1606
+ }
1607
+
1608
+ $sourceDir = Join-Path $DistDir $ModuleName
1609
+ Compress-Archive -Path "$sourceDir\*" -DestinationPath $zipPath -Force
1610
+ Write-Log "Contents zipped into '$ModuleName.zip'."
1611
+ }
1612
+
1613
+ function Get-JsonValue {
1614
+ param(
1615
+ [string]$FilePath,
1616
+ [string]$PropertyName
1617
+ )
1618
+ if (-not (Test-Path $FilePath)) { return $null }
1619
+ try {
1620
+ $json = Get-Content $FilePath -Raw | ConvertFrom-Json
1621
+ return $json.$PropertyName
1622
+ } catch {
1623
+ Write-Log "Failed to parse JSON from $FilePath"
1624
+ return $null
1625
+ }
1626
+ }
1627
+
1628
+ function Main {
1629
+ if ($PSScriptRoot) {
1630
+ $scriptDir = $PSScriptRoot
1631
+ } elseif ($MyInvocation.MyCommand.Path) {
1632
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1633
+ } else {
1634
+ $scriptDir = (Get-Location).Path
1635
+ }
1636
+ $moduleBaseDir = Split-Path -Parent $scriptDir
1637
+ $pluginDir = Join-Path $moduleBaseDir "plugins"
1638
+ $pluginRulesDir = Join-Path $pluginDir "rules"
1639
+ $distDir = Join-Path $moduleBaseDir "dist"
1640
+ $resourcesDir = Join-Path $moduleBaseDir "resources"
1641
+
1642
+ if ($Script:BuildPlugins) {
1643
+ Write-Log "Building plugins..."
1644
+ Push-Location $pluginDir
1645
+ try {
1646
+ & mvn clean package
1647
+ if ($LASTEXITCODE -ne 0) {
1648
+ Write-Error "Maven build failed"
1649
+ exit 1
1650
+ }
1651
+ } finally {
1652
+ Pop-Location
1653
+ }
1654
+ }
1655
+
1656
+ $moduleJsonPath = Join-Path $resourcesDir "module.json"
1657
+ $moduleName = Get-JsonValue $moduleJsonPath "name"
1658
+ $moduleVersion = Get-JsonValue $moduleJsonPath "version"
1659
+ if (-not $moduleName -or -not $moduleVersion) {
1660
+ Write-Error "Could not extract module name or version from module.json"
1661
+ exit 1
1662
+ }
1663
+
1664
+ $sanitizedName = $moduleName -replace "/", "-"
1665
+ $combinedName = "$sanitizedName-$moduleVersion"
1666
+ $moduleDistDir = Join-Path $distDir $combinedName
1667
+ $moduleDistAssetsDir = Join-Path $moduleDistDir "assets"
1668
+ $moduleDistPluginsDir = Join-Path $moduleDistAssetsDir "rules"
1669
+
1670
+ Remove-FolderIfExists $distDir
1671
+ New-Item -ItemType Directory -Path $moduleDistDir -Force | Out-Null
1672
+
1673
+ $rootFiles = @("CHANGELOG.md", "README.md", "LICENSE.md")
1674
+ foreach ($file in $rootFiles) {
1675
+ $sourcePath = Join-Path $moduleBaseDir $file
1676
+ if (Test-Path $sourcePath) {
1677
+ Copy-Item $sourcePath $moduleDistDir
1678
+ }
1679
+ }
1680
+
1681
+ Copy-Assets $resourcesDir $moduleDistDir $moduleDistAssetsDir
1682
+ Copy-JarFiles $pluginRulesDir $moduleDistPluginsDir
1683
+
1684
+ if ($Script:BuildModule) {
1685
+ Write-Log "Building module..."
1686
+ New-ModuleZip $distDir $combinedName
1687
+ }
1688
+
1689
+ Write-Host "Build completed successfully!"
1690
+ }
1691
+
1692
+ Main
1693
+ ```
1694
+
1695
+ ### `.gitignore`
1696
+
1697
+ ```gitignore
1698
+ # Maven build output
1699
+ target/
1700
+ *.class
1701
+
1702
+ # IDE files
1703
+ .idea/
1704
+ *.iml
1705
+ .project
1706
+ .classpath
1707
+ .settings/
1708
+ .vscode/
1709
+
1710
+ # Distribution output
1711
+ dist/
1712
+
1713
+ # OS files
1714
+ .DS_Store
1715
+ Thumbs.db
1716
+
1717
+ # Logs
1718
+ *.log
1719
+
1720
+ # Dependencies (should be resolved by Maven)
1721
+ node_modules/
1722
+
1723
+ # Schema (downloaded separately)
1724
+ global/schema.json
1725
+ ```
1726
+
1727
+ ## Entity Type to Package Mapping
1728
+
1729
+ When `--entity-types` is specified, rule classes are placed in sub-packages that match the entity type:
1730
+
1731
+ | Entity Type | Java Package | Sub-package name |
1732
+ |-------------|-------------|------------------|
1733
+ | `ORDER` | `com.fluentcommerce.rule.order` | `order` |
1734
+ | `FULFILMENT` | `com.fluentcommerce.rule.fulfilment` | `fulfilment` |
1735
+ | `FULFILMENT_OPTIONS` | `com.fluentcommerce.rule.fulfilmentoption` | `fulfilmentoption` |
1736
+ | `ARTICLE` | `com.fluentcommerce.rule.article` | `article` |
1737
+ | `CONSIGNMENT` | `com.fluentcommerce.rule.consignment` | `consignment` |
1738
+ | `WAVE` | `com.fluentcommerce.rule.wave` | `wave` |
1739
+ | `LOCATION` | `com.fluentcommerce.rule.location` | `location` |
1740
+ | `PRODUCT` | `com.fluentcommerce.rule.variantproduct` | `variantproduct` |
1741
+ | `VIRTUAL_CATALOGUE` | `com.fluentcommerce.rule.catalogue` | `catalogue` |
1742
+ | `INVENTORY_POSITION` | `com.fluentcommerce.rule.inventory` | `inventory` |
1743
+ | `RETURN_ORDER` | `com.fluentcommerce.rule.returnorder` | `returnorder` |
1744
+ | (default/common) | `com.fluentcommerce.rule.common` | `common` |
1745
+
1746
+ When multiple entity types are specified, place each rule in the package matching its primary entity type. If a rule applies to multiple entity types, use `common`.
1747
+
1748
+ ## Execution Flow
1749
+
1750
+ ```
1751
+ 1. VALIDATE inputs
1752
+ a. module-name format: must match /^[a-z][a-z0-9-]*$/ (lowercase, hyphens allowed, no leading hyphen)
1753
+ b. rule names (if --rules): must match /^[A-Z][a-zA-Z0-9]*$/ (PascalCase)
1754
+ c. entity types (if --entity-types): must be in the valid set above
1755
+
1756
+ 2. PRE-CHECK: Module already exists?
1757
+ a. Check accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-<module-name>/ exists
1758
+ b. If exists → ABORT with guidance
1759
+ c. Optionally check deployed modules via: fluent module list -p <PROFILE>
1760
+
1761
+ 3. DETECT account prefix
1762
+ a. If --account-prefix provided → use it
1763
+ b. Else try MCP: plugin.list (compact: true)
1764
+ → Extract <ACCOUNT> from first non-FLUENTRETAIL rule key
1765
+ c. Else derive from PROFILE name (uppercase)
1766
+ d. Fallback: "UNKNOWN" with warning
1767
+
1768
+ 4. DETECT SDK versions (optional)
1769
+ a. Check if another module exists under accounts/<PROFILE>/SOURCE/
1770
+ b. If found, read its rules/*/pom.xml for rubix-plugin-base.version and fluent-api-client.version
1771
+ c. If not found, use defaults: rubix-plugin-base=1.2.0.679, fluent-api-client=1.2.0.118
1772
+
1773
+ 5. COMPUTE derived values
1774
+ a. MODULE_ALIAS = <module-name> (e.g., hm-returns)
1775
+ b. MODULE_TITLE = title-case of module-name with hyphens as spaces (e.g., Hm Returns)
1776
+ c. OSGI_SYMBOLIC_NAME = "_." + <module-alias with hyphens removed> (e.g., _.hmreturns)
1777
+ d. PACKAGE = entity type sub-package from mapping table above
1778
+
1779
+ 6. CREATE directory structure
1780
+ a. Create all directories listed in the structure section
1781
+ b. Ensure all parent directories exist before writing files
1782
+
1783
+ 7. GENERATE files (use Write tool, not bash echo)
1784
+ a. plugins/pom.xml — parent POM with all variable substitutions
1785
+ b. plugins/types/pom.xml — types aggregator
1786
+ c. plugins/types/<alias>/pom.xml — types implementation
1787
+ d. plugins/util/pom.xml — util aggregator
1788
+ e. plugins/util/<alias>/pom.xml — util implementation
1789
+ f. plugins/rules/pom.xml — rules aggregator
1790
+ g. plugins/rules/<alias>/pom.xml — rules implementation (OSGi bundle)
1791
+ h. resources/module.json — module manifest
1792
+ i. scripts/build-module.sh — bash build script (set executable: chmod +x)
1793
+ j. scripts/build-module.ps1 — powershell build script
1794
+ k. .gitignore
1795
+
1796
+ 8. FOR EACH rule in --rules (if specified):
1797
+ a. Determine target package from entity type
1798
+ b. Generate Java rule class using the appropriate template (BaseRule or Rule)
1799
+ c. Generate test class
1800
+ d. Add rule name to module.json rules[] array
1801
+
1802
+ 9. CREATE empty placeholder directories
1803
+ a. resources/settings/
1804
+ b. global/
1805
+ c. dist/
1806
+ d. Empty src/main/java/ trees for types and util (if no rules specified)
1807
+
1808
+ 10. MAKE build script executable
1809
+ bash: chmod +x scripts/build-module.sh
1810
+
1811
+ 11. VERIFY structure (optional, if Maven is available)
1812
+ cd plugins/ && mvn validate
1813
+ This confirms POM structure is valid without actually building.
1814
+
1815
+ 12. REPORT generated files
1816
+ List all files created with their full paths.
1817
+ Print next steps:
1818
+ - "Module scaffolded at: accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-<module-name>/"
1819
+ - "Next: Add rules with /fluent-rule-scaffold or build with /fluent-build"
1820
+ - "Validate structure: /fluent-module-validate"
1821
+ ```
1822
+
1823
+ ## SDK Version Detection
1824
+
1825
+ The `rubix-plugin-base.version` and `fluent-api-client.version` determine Fluent Commerce SDK compatibility. Detection order:
1826
+
1827
+ ```
1828
+ 1. Check existing modules under accounts/<PROFILE>/SOURCE/
1829
+ → Read any rules/*/pom.xml for <rubix-plugin-base.version> and <fluent-api-client.version>
1830
+ → Reuse the same versions for consistency
1831
+
1832
+ 2. Check deployed modules via MCP (environment.discover with modules section)
1833
+ → Extract compatibility block versions from deployed module metadata
1834
+
1835
+ 3. Fall back to known stable versions:
1836
+ rubix-plugin-base.version = 1.2.0.679
1837
+ fluent-api-client.version = 1.2.0.118
1838
+ apollo-client-maven-plugin.version = 1.0.0.15
1839
+ ```
1840
+
1841
+ ## Integration with Other Skills
1842
+
1843
+ | Skill | Relationship |
1844
+ |-------|-------------|
1845
+ | `/fluent-rule-scaffold` | For adding rules to an EXISTING module. After module-scaffold creates the module structure, use rule-scaffold to add individual rules incrementally. Rule-scaffold handles: OOTB-vs-custom decision, BaseRule vs Rule template selection, module.json wiring, and test generation. |
1846
+ | `/fluent-build` | Immediately usable after scaffold. Run `mvn clean install` from `plugins/`, then `scripts/build-module.sh` (or `.ps1`) for ZIP packaging. |
1847
+ | `/fluent-module-validate` | Should pass structural validation on the scaffolded output with 0 FAILs. Expected WARNs: no GraphQL schema (not yet downloaded), no distribution (not yet built). |
1848
+ | `/fluent-module-deploy` | Deploy the built ZIP to a target retailer after build succeeds. |
1849
+ | `/fluent-custom-code` | Analyzes the generated source for behavior mapping and constraint extraction. |
1850
+
1851
+ ### Post-Scaffold Workflow
1852
+
1853
+ ```
1854
+ /fluent-module-scaffold hm-returns --entity-types ORDER,RETURN_ORDER --rules CreateReturnFromOrder,ProcessReturnApproval
1855
+ |
1856
+ v
1857
+ /fluent-rule-scaffold AddReturnNotes --entity-type RETURN_ORDER --package returnorder
1858
+ |
1859
+ v
1860
+ /fluent-module-validate accounts/<PROFILE>/SOURCE/fluentcommerce-fc-module-hm-returns/
1861
+ |
1862
+ v
1863
+ /fluent-build build
1864
+ |
1865
+ v
1866
+ /fluent-module-deploy
1867
+ ```
1868
+
1869
+ ## Edge Cases
1870
+
1871
+ ### Module name conflicts
1872
+
1873
+ Check `accounts/<PROFILE>/SOURCE/` for any directory containing the module name:
1874
+
1875
+ ```bash
1876
+ ls accounts/<PROFILE>/SOURCE/ | grep -i "<module-name>"
1877
+ ```
1878
+
1879
+ If found, abort. Do NOT overwrite an existing module.
1880
+
1881
+ ### No rules specified
1882
+
1883
+ When `--rules` is omitted:
1884
+ - `module.json` has `"rules": []`
1885
+ - No Java classes are generated
1886
+ - Empty `src/main/java/com/fluentcommerce/rule/` directory is created
1887
+ - Empty `src/test/java/com/fluentcommerce/rule/` directory is created
1888
+ - The module is still buildable (produces an empty JAR)
1889
+
1890
+ ### Windows path handling
1891
+
1892
+ - All POM `<module>` references use forward slashes (Maven standard)
1893
+ - Build scripts have both `.sh` (Git Bash, WSL, macOS, Linux) and `.ps1` (PowerShell) variants
1894
+ - Use Node.js for cross-platform directory creation when bash `mkdir -p` is unavailable
1895
+ - Use `path.join()` semantics in documentation, not hardcoded separators
1896
+
1897
+ ### Module name validation
1898
+
1899
+ Reject module names that:
1900
+ - Start with a digit or hyphen
1901
+ - Contain uppercase letters (convention: lowercase only)
1902
+ - Contain spaces or special characters beyond hyphens
1903
+ - Are empty or longer than 50 characters
1904
+
1905
+ ### GraphQL schema not available
1906
+
1907
+ The `global/schema.json` file is required for Apollo codegen (types submodule) but may not be available at scaffold time. The scaffolder:
1908
+ - Creates the `global/` directory empty
1909
+ - Notes in the output that the schema must be downloaded before a full build
1910
+ - The types submodule will fail codegen without it, but `mvn validate` will pass
1911
+
1912
+ ### Duplicate rule names
1913
+
1914
+ If `--rules` contains duplicates, deduplicate silently and warn:
1915
+
1916
+ ```
1917
+ Warning: Duplicate rule name 'MyRule' removed from list.
1918
+ ```
1919
+
1920
+ ## Gotchas
1921
+
1922
+ - **OSGi symbolic name** must be unique per account. Convention: `_.<aliasNoHyphens>` (e.g., `_.hmreturns`). If two modules share the same symbolic name, deployment will fail with a cryptic OSGi error.
1923
+ - **`<Rubix-Account>_</Rubix-Account>`** in the bundle plugin config means "register to the deploying account." Do not change this to a hardcoded account name.
1924
+ - **Java source level 1.8** is required by the Rubix runtime. Do not use Java 11+ features in rule code even if the build JDK is newer.
1925
+ - **`module.json` `name` field** uses the format `fluent-commerce/fc-module-<name>` by convention (with the `fluent-commerce/` prefix). This is the canonical identifier used by `fluent module install`.
1926
+ - **`module.json` `rules` array** contains simple rule names (e.g., `"MyRule"`), NOT fully-qualified class names or account-prefixed keys. The OSGi bundle plugin discovers `@RuleInfo` annotations at build time.
1927
+ - **Apollo codegen** requires Node.js 18 for the Maven plugin. On Windows with nvm-windows (nvm4w), use: `nvm use 18.x.x` (full version number required).
1928
+ - **Intermediate POM hierarchy** matters. The parent POM declares `<module>types</module>`, which references `plugins/types/pom.xml` (an aggregator), which in turn declares `<module>${MODULE_ALIAS}</module>` pointing to the implementation POM. Skipping the aggregator level will break the build.