@nanobpm/nano-ide-app-embedded-graalvm-native 1.0.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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "embedded-graalvm-native",
3
+ "kind": "app",
4
+ "displayName": "Embedded Nano (GraalVM Native Image)",
5
+ "fileTypes": [{ "ext": ".java", "monacoLang": "java" }],
6
+ "templates": [
7
+ { "id": "embedded-graalvm-starter", "label": "KYC microservice (GraalVM native) — same demo, ~30 MB standalone binary (ADR 0005)" }
8
+ ],
9
+ "toolchain": {
10
+ "detect": ["mvn", "-version"],
11
+ "run": ["./microservice/target/kyc-microservice"],
12
+ "compile": ["mvn", "-q", "-f", "microservice/pom.xml", "-Pnative", "-DskipTests", "package"],
13
+ "targets": []
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@nanobpm/nano-ide-app-embedded-graalvm-native",
3
+ "version": "1.0.0",
4
+ "description": "GraalVM Native Image variant of the embedded Nano JVM template: same Bernd-in-JVM code, compiled to a single standalone binary (~30 MB, no JDK required at runtime).",
5
+ "license": "Apache-2.0",
6
+ "keywords": ["nano-ide-ext", "nano-ide-app", "nanobpm", "embedded", "graalvm", "native-image", "bernd"],
7
+ "publishConfig": { "access": "public" },
8
+ "repository": { "type": "git", "url": "https://github.com/jwulf/nano-ide.git", "directory": "packages/app-embedded-graalvm-native" },
9
+ "files": ["nano-ide.ext.json", "templates"]
10
+ }
@@ -0,0 +1,64 @@
1
+ # Embedded Nano — KYC microservice (GraalVM native binary)
2
+
3
+ Same demo as `@nanobpm/nano-ide-app-embedded-jvm` — orchestration-of-
4
+ orchestration with an outer BPMN on the Nano IDE gateway and an inner
5
+ BPMN on a Bernd engine embedded in this microservice — but packaged as
6
+ a **single ~30 MB standalone binary** via GraalVM Native Image. No JVM
7
+ required at deploy time; ship a scratch container.
8
+
9
+ See the sibling JVM template's README for the full architectural walk-
10
+ through. This README covers only the native-image differences.
11
+
12
+ ## Prerequisites
13
+
14
+ - **GraalVM for JDK 21+** with `native-image` on `PATH`
15
+ ```sh
16
+ sdk install java 21.0.4-graal
17
+ ```
18
+ - Maven 3.9+
19
+
20
+ ## Build
21
+
22
+ ```sh
23
+ cd microservice
24
+ mvn -q -Pnative -DskipTests package
25
+ ```
26
+
27
+ Produces `microservice/target/kyc-microservice` (~30 MB). Copy anywhere
28
+ and run.
29
+
30
+ ## Run
31
+
32
+ ```sh
33
+ ./microservice/target/kyc-microservice
34
+ # defaults to ws://localhost:8080/falcon; override:
35
+ FALCON_URL=ws://gateway.example.com/falcon ./microservice/target/kyc-microservice
36
+ ```
37
+
38
+ ## What's inside the binary
39
+
40
+ - `com.example.KycMicroservice` (main class)
41
+ - The `kyc.bpmn` XML (packaged as a resource; kept by
42
+ `-H:IncludeResources=kyc/.*\.bpmn`)
43
+ - The Nano engine's wasm blob shipped inside `nano-bernd` (kept by
44
+ `-H:IncludeResources=nano-bernd/.*`)
45
+ - Chicory (pure-Java wasm interpreter — AOT-compiles cleanly)
46
+ - Jackson + java.net.http WebSocket for the outer Falcon transport
47
+
48
+ ## Reflection config
49
+
50
+ `microservice/src/main/resources/META-INF/native-image/reflect-config.json`
51
+ flags the `EmbeddedEngine`, `ActivatedJob`, and `WasmManifest` types
52
+ because `EmbeddedNanoTransport` (in `camunda-client-java-falcon`) uses
53
+ reflection to talk to them — Graal's closed-world compilation needs this
54
+ so the methods aren't stripped.
55
+
56
+ ## Notes
57
+
58
+ - `native-image` uses ~4 GB RAM during compilation (`-J-Xmx4g` in the pom).
59
+ - The resulting binary is dynamically-linked against libc; for fully-
60
+ static musl binaries add `--static --libc=musl` to `<buildArgs>` and
61
+ build on a Linux host with `musl-cross-compile`.
62
+ - Cold start is single-digit milliseconds, so this variant is a good fit
63
+ for scale-to-zero / short-lived scheduled jobs where JVM warm-up would
64
+ dominate wall-clock time.
@@ -0,0 +1,101 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+ <modelVersion>4.0.0</modelVersion>
6
+
7
+ <groupId>com.example</groupId>
8
+ <artifactId>kyc-microservice-native</artifactId>
9
+ <version>0.1.0</version>
10
+ <packaging>jar</packaging>
11
+
12
+ <name>KYC Microservice (GraalVM native)</name>
13
+ <description>
14
+ Same code as the JVM embedded starter, packaged as a GraalVM Native
15
+ Image single binary (~30 MB). The KYC microservice — including its
16
+ embedded Bernd engine and the packaged kyc.bpmn — becomes a
17
+ standalone executable with no JVM required at runtime.
18
+ </description>
19
+
20
+ <properties>
21
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
22
+ <maven.compiler.source>21</maven.compiler.source>
23
+ <maven.compiler.target>21</maven.compiler.target>
24
+
25
+ <camunda-client.version>1.1.0-SNAPSHOT</camunda-client.version>
26
+ <nano-bernd.version>0.2.0</nano-bernd.version>
27
+ <native.maven.plugin.version>0.10.3</native.maven.plugin.version>
28
+ </properties>
29
+
30
+ <dependencies>
31
+ <dependency>
32
+ <groupId>io.github.jwulf</groupId>
33
+ <artifactId>camunda-client-java-falcon</artifactId>
34
+ <version>${camunda-client.version}</version>
35
+ </dependency>
36
+ <dependency>
37
+ <groupId>io.github.jwulf</groupId>
38
+ <artifactId>nano-bernd</artifactId>
39
+ <version>${nano-bernd.version}</version>
40
+ </dependency>
41
+ <dependency>
42
+ <groupId>org.slf4j</groupId>
43
+ <artifactId>slf4j-simple</artifactId>
44
+ <version>2.0.13</version>
45
+ </dependency>
46
+ <dependency>
47
+ <groupId>org.junit.jupiter</groupId>
48
+ <artifactId>junit-jupiter</artifactId>
49
+ <version>5.11.0</version>
50
+ <scope>test</scope>
51
+ </dependency>
52
+ </dependencies>
53
+
54
+ <profiles>
55
+ <profile>
56
+ <id>native</id>
57
+ <build>
58
+ <plugins>
59
+ <plugin>
60
+ <groupId>org.graalvm.buildtools</groupId>
61
+ <artifactId>native-maven-plugin</artifactId>
62
+ <version>${native.maven.plugin.version}</version>
63
+ <extensions>true</extensions>
64
+ <executions>
65
+ <execution>
66
+ <id>build-native</id>
67
+ <goals><goal>compile-no-fork</goal></goals>
68
+ <phase>package</phase>
69
+ </execution>
70
+ </executions>
71
+ <configuration>
72
+ <imageName>kyc-microservice</imageName>
73
+ <mainClass>com.example.KycMicroservice</mainClass>
74
+ <buildArgs>
75
+ <buildArg>--no-fallback</buildArg>
76
+ <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
77
+ <!-- Embed both BPMN files and the wasm engine into the binary. -->
78
+ <buildArg>-H:IncludeResources=kyc/.*\.bpmn</buildArg>
79
+ <buildArg>-H:IncludeResources=nano-bernd/nano_engine\.wasm</buildArg>
80
+ <buildArg>-H:IncludeResources=nano-bernd/manifest\.json</buildArg>
81
+ <buildArg>-J-Xmx4g</buildArg>
82
+ </buildArgs>
83
+ </configuration>
84
+ </plugin>
85
+ </plugins>
86
+ </build>
87
+ </profile>
88
+ </profiles>
89
+
90
+ <build>
91
+ <plugins>
92
+ <plugin>
93
+ <artifactId>maven-jar-plugin</artifactId>
94
+ <version>3.4.2</version>
95
+ <configuration>
96
+ <archive><manifest><mainClass>com.example.KycMicroservice</mainClass></manifest></archive>
97
+ </configuration>
98
+ </plugin>
99
+ </plugins>
100
+ </build>
101
+ </project>
@@ -0,0 +1,204 @@
1
+ /*
2
+ * KYC Microservice — embedded-Nano demo (ADR 0005).
3
+ *
4
+ * [ Nano IDE gateway ] [ This JVM process ]
5
+ * │ │
6
+ * models/onboarding.bpmn microservice/
7
+ * (auto-deployed by IDE) └─ src/main/resources/kyc/kyc.bpmn
8
+ * │ │
9
+ * │ Falcon WebSocket (NanoTransport.falcon) │
10
+ * │◄──────────────── verify_kyc ────────────────┤
11
+ * │ │
12
+ * │ ┌───────┴───────┐
13
+ * │ starts inner instance
14
+ * │ on in-process Bernd engine
15
+ * │ (NanoTransport.embedded)
16
+ * │ │
17
+ * │ check_id → screen_sanctions → check_pep → risk_score
18
+ * │ │
19
+ * │ aggregates result to a KYC decision
20
+ * │ │
21
+ * │◄──── completeJob(decision) ────────┘
22
+ *
23
+ * Both engines are BPMN. Both are auditable. Onboarding team owns the
24
+ * outer file; compliance owns kyc.bpmn.
25
+ */
26
+ package com.example;
27
+
28
+ import com.fasterxml.jackson.databind.JsonNode;
29
+ import com.nanobpm.camunda.falcon.FalconTransport;
30
+ import com.nanobpm.camunda.transport.NanoTransport;
31
+ import io.github.jwulf.nano.bernd.EmbeddedEngine;
32
+ import java.io.InputStream;
33
+ import java.net.URI;
34
+ import java.nio.charset.StandardCharsets;
35
+ import java.util.Map;
36
+ import java.util.concurrent.CountDownLatch;
37
+ import java.util.concurrent.TimeUnit;
38
+
39
+ public final class KycMicroservice {
40
+
41
+ private static final String KYC_BPMN_RESOURCE = "/kyc/kyc.bpmn";
42
+
43
+ // Set FALCON_URL to override; defaults to the Nano IDE gateway's Falcon endpoint.
44
+ private static final String DEFAULT_FALCON_URL = "ws://localhost:8080/falcon";
45
+
46
+ public static void main(final String[] args) throws Exception {
47
+ final URI falconUrl = URI.create(
48
+ System.getenv().getOrDefault("FALCON_URL", DEFAULT_FALCON_URL));
49
+
50
+ // 1. Boot the in-process Bernd engine and deploy the inner KYC flow.
51
+ final EmbeddedEngine kycEngine = EmbeddedEngine.create();
52
+ final String kycBpmn = loadResource(KYC_BPMN_RESOURCE);
53
+ kycEngine.deploy(kycBpmn);
54
+ System.out.println("[boot] Bernd engine up; kyc.bpmn deployed (engine v"
55
+ + kycEngine.manifest().engineVersion() + ", ABI v" + kycEngine.manifest().abiVersion() + ")");
56
+
57
+ // 2. Open the Falcon transport to the outer gateway.
58
+ final NanoTransport outer = NanoTransport.falcon(falconUrl);
59
+ outer.connect().get(5, TimeUnit.SECONDS);
60
+ System.out.println("[boot] connected to outer gateway at " + falconUrl);
61
+
62
+ // 3. Subscribe to the outer 'verify_kyc' job. Each activation runs the
63
+ // inner kyc.bpmn to completion on the embedded engine, then acks the
64
+ // outer job with a { decision: approved|manual_review|rejected }
65
+ // variable so the outer gateway routes correctly.
66
+ // NOTE: maxJobs=1 (serial). The embedded engine + per-instance KycContext
67
+ // are shared state driven by driveInnerFlow, so handling multiple outer
68
+ // verify_kyc activations concurrently would let inner activateJobs polls
69
+ // race across KycContexts. For higher throughput, run several
70
+ // microservice instances (or refactor to a per-invocation engine).
71
+ outer.subscribe(new FalconTransport.Subscription(
72
+ "verify_kyc", "kyc-microservice", 1, 30_000L, null,
73
+ (JsonNode job) -> handleVerifyKyc(job, kycEngine, outer))
74
+ ).get(5, TimeUnit.SECONDS);
75
+ System.out.println("[ready] subscribed to verify_kyc — start an onboarding instance in the IDE");
76
+
77
+ // Keep alive.
78
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
79
+ try { outer.close(); } catch (final Exception ignored) { /* best effort */ }
80
+ try { kycEngine.close(); } catch (final Exception ignored) { /* best effort */ }
81
+ }));
82
+ new CountDownLatch(1).await();
83
+ }
84
+
85
+ private static void handleVerifyKyc(
86
+ final JsonNode job, final EmbeddedEngine kycEngine, final NanoTransport outer) {
87
+ final String outerJobKey = job.get("jobKey").asText();
88
+ final JsonNode vars = job.path("variables");
89
+ final String customerId = vars.path("customerId").asText("cust-unknown");
90
+
91
+ System.out.println();
92
+ System.out.println("═══════════════════════════════════════════════════════════════");
93
+ System.out.println("[outer] verify_kyc activated (jobKey=" + outerJobKey + ", customer=" + customerId + ")");
94
+ System.out.println("[inner] starting kyc.bpmn instance for " + customerId);
95
+
96
+ // Run the inner flow on Bernd. Aggregate check results in the context.
97
+ // If it throws, log and return WITHOUT completing the outer job so
98
+ // Nano's job timeout drives a retry.
99
+ final KycContext ctx = new KycContext(customerId);
100
+ final String decision;
101
+ try {
102
+ final String innerInstance = kycEngine.createInstance("kyc");
103
+ System.out.println("[inner] instance " + innerInstance + " running");
104
+ driveInnerFlow(kycEngine, ctx);
105
+ decision = ctx.aggregate();
106
+ } catch (final Exception e) {
107
+ System.err.println("[inner] flow failed for customer=" + customerId + ": " + e);
108
+ e.printStackTrace(System.err);
109
+ System.err.println("[outer] NOT completing verify_kyc — outer job will time out and retry");
110
+ return;
111
+ }
112
+
113
+ System.out.println("[inner] kyc.bpmn done — decision: " + decision);
114
+ System.out.println("[outer] completing verify_kyc with decision=" + decision);
115
+ System.out.println("═══════════════════════════════════════════════════════════════");
116
+
117
+ try {
118
+ // ABI v2 embedded engine ignores completeJob variables; but the OUTER
119
+ // gateway is the full Nano engine and does accept them, so the
120
+ // exclusive gateway in onboarding.bpmn can branch on `decision`.
121
+ outer.completeJob(outerJobKey, Map.of("decision", decision, "customerId", customerId))
122
+ .get(5, TimeUnit.SECONDS);
123
+ } catch (final Exception e) {
124
+ System.err.println("[outer] complete failed: " + e.getMessage());
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Drive the inner flow by polling activateJobs on the embedded engine for
130
+ * each step type in order. Each step is a Java lambda that mutates the
131
+ * context and prints a console line — like a Camunda job worker, but
132
+ * in-process. Once the last step's job completes and the inner instance
133
+ * reaches its end event, this method returns.
134
+ */
135
+ private static void driveInnerFlow(final EmbeddedEngine engine, final KycContext ctx) {
136
+ step(engine, ctx, "check_id", (c) -> {
137
+ c.idValid = true; // real impl would OCR + verify against issuing authority
138
+ System.out.println(" [inner:check_id] ID document verified ✓");
139
+ });
140
+ step(engine, ctx, "screen_sanctions", (c) -> {
141
+ c.sanctionsHit = false;
142
+ System.out.println(" [inner:screen_sanctions] OFAC + UN lists clear ✓");
143
+ });
144
+ step(engine, ctx, "check_pep", (c) -> {
145
+ c.pepMatch = c.customerId.startsWith("vip-"); // demo: 'vip-*' triggers PEP path
146
+ System.out.println(" [inner:check_pep] PEP list " + (c.pepMatch ? "MATCH ⚠" : "clear ✓"));
147
+ });
148
+ step(engine, ctx, "risk_score", (c) -> {
149
+ // Demo heuristic. Real world: a rules engine or ML model.
150
+ c.riskScore = c.pepMatch ? 75 : (c.sanctionsHit ? 100 : 15);
151
+ System.out.println(" [inner:risk_score] score = " + c.riskScore);
152
+ });
153
+ }
154
+
155
+ /** Poll activateJobs for one job of the given type, run the lambda, complete. */
156
+ private static void step(
157
+ final EmbeddedEngine engine, final KycContext ctx, final String type,
158
+ final java.util.function.Consumer<KycContext> handler) {
159
+ final long deadline = System.currentTimeMillis() + 5_000L;
160
+ while (System.currentTimeMillis() < deadline) {
161
+ final var jobs = engine.activateJobs(type, "kyc-inner", 1, 30_000L);
162
+ if (!jobs.isEmpty()) {
163
+ final var job = jobs.get(0);
164
+ handler.accept(ctx);
165
+ engine.completeJob(job.key());
166
+ return;
167
+ }
168
+ try {
169
+ Thread.sleep(10);
170
+ } catch (final InterruptedException ie) {
171
+ Thread.currentThread().interrupt();
172
+ throw new IllegalStateException(
173
+ "inner flow interrupted while waiting for " + type + " job", ie);
174
+ }
175
+ }
176
+ throw new IllegalStateException("inner flow stalled: no " + type + " job appeared within 5s");
177
+ }
178
+
179
+ private static String loadResource(final String path) throws Exception {
180
+ try (InputStream in = KycMicroservice.class.getResourceAsStream(path)) {
181
+ if (in == null) throw new IllegalStateException("missing resource: " + path);
182
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
183
+ }
184
+ }
185
+
186
+ /** Per-customer aggregation of the inner check results. */
187
+ static final class KycContext {
188
+ final String customerId;
189
+ boolean idValid;
190
+ boolean sanctionsHit;
191
+ boolean pepMatch;
192
+ int riskScore;
193
+
194
+ KycContext(final String customerId) { this.customerId = customerId; }
195
+
196
+ String aggregate() {
197
+ if (!idValid || sanctionsHit) return "rejected";
198
+ if (pepMatch || riskScore >= 70) return "manual_review";
199
+ return "approved";
200
+ }
201
+ }
202
+
203
+ private KycMicroservice() {}
204
+ }
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "name": "io.github.jwulf.nano.bernd.EmbeddedEngine",
4
+ "allDeclaredConstructors": true,
5
+ "allPublicConstructors": true,
6
+ "allDeclaredMethods": true,
7
+ "allPublicMethods": true,
8
+ "allDeclaredFields": true
9
+ },
10
+ {
11
+ "name": "io.github.jwulf.nano.bernd.ActivatedJob",
12
+ "allDeclaredConstructors": true,
13
+ "allPublicConstructors": true,
14
+ "allDeclaredMethods": true,
15
+ "allPublicMethods": true,
16
+ "allDeclaredFields": true
17
+ },
18
+ {
19
+ "name": "io.github.jwulf.nano.bernd.WasmManifest",
20
+ "allDeclaredConstructors": true,
21
+ "allPublicConstructors": true,
22
+ "allDeclaredMethods": true,
23
+ "allPublicMethods": true,
24
+ "allDeclaredFields": true
25
+ }
26
+ ]
@@ -0,0 +1,9 @@
1
+ {
2
+ "resources": {
3
+ "includes": [
4
+ { "pattern": "kyc/.*\\.bpmn" },
5
+ { "pattern": "nano-bernd/nano_engine\\.wasm" },
6
+ { "pattern": "nano-bernd/manifest\\.json" }
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,54 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
4
+ targetNamespace="http://example.com/kyc"
5
+ id="kyc_defs">
6
+
7
+ <!--
8
+ KYC verification — the INNER process, embedded into the microservice jar
9
+ (Maven picks up src/main/resources/kyc/kyc.bpmn automatically) and
10
+ orchestrated by an in-process Bernd engine (io.github.jwulf:nano-bernd).
11
+
12
+ Compliance owns this file. Add a step here (say, "check_adverse_media")
13
+ and bump the microservice — the onboarding team's outer flow doesn't
14
+ change.
15
+
16
+ ABI v2 note: variable-driven gateways aren't wired to inner engines yet,
17
+ so this flow is a linear sequence of checks. The microservice aggregates
18
+ the per-check results in Java-land and returns a single 'decision' to
19
+ the outer flow. When ABI v3 exposes variables-on-complete, branches
20
+ like "sanctions_hit → straight-to-rejected" move in here.
21
+ -->
22
+
23
+ <bpmn:process id="kyc" name="KYC Verification" isExecutable="true">
24
+ <bpmn:startEvent id="s"><bpmn:outgoing>f1</bpmn:outgoing></bpmn:startEvent>
25
+
26
+ <bpmn:serviceTask id="check_id" name="Verify ID document">
27
+ <bpmn:extensionElements><zeebe:taskDefinition type="check_id" /></bpmn:extensionElements>
28
+ <bpmn:incoming>f1</bpmn:incoming><bpmn:outgoing>f2</bpmn:outgoing>
29
+ </bpmn:serviceTask>
30
+
31
+ <bpmn:serviceTask id="screen_sanctions" name="Screen OFAC / UN sanctions">
32
+ <bpmn:extensionElements><zeebe:taskDefinition type="screen_sanctions" /></bpmn:extensionElements>
33
+ <bpmn:incoming>f2</bpmn:incoming><bpmn:outgoing>f3</bpmn:outgoing>
34
+ </bpmn:serviceTask>
35
+
36
+ <bpmn:serviceTask id="check_pep" name="Check politically-exposed person list">
37
+ <bpmn:extensionElements><zeebe:taskDefinition type="check_pep" /></bpmn:extensionElements>
38
+ <bpmn:incoming>f3</bpmn:incoming><bpmn:outgoing>f4</bpmn:outgoing>
39
+ </bpmn:serviceTask>
40
+
41
+ <bpmn:serviceTask id="risk_score" name="Compute risk score">
42
+ <bpmn:extensionElements><zeebe:taskDefinition type="risk_score" /></bpmn:extensionElements>
43
+ <bpmn:incoming>f4</bpmn:incoming><bpmn:outgoing>f5</bpmn:outgoing>
44
+ </bpmn:serviceTask>
45
+
46
+ <bpmn:endEvent id="e"><bpmn:incoming>f5</bpmn:incoming></bpmn:endEvent>
47
+
48
+ <bpmn:sequenceFlow id="f1" sourceRef="s" targetRef="check_id" />
49
+ <bpmn:sequenceFlow id="f2" sourceRef="check_id" targetRef="screen_sanctions" />
50
+ <bpmn:sequenceFlow id="f3" sourceRef="screen_sanctions" targetRef="check_pep" />
51
+ <bpmn:sequenceFlow id="f4" sourceRef="check_pep" targetRef="risk_score" />
52
+ <bpmn:sequenceFlow id="f5" sourceRef="risk_score" targetRef="e" />
53
+ </bpmn:process>
54
+ </bpmn:definitions>
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Smoke test: exercise the KycMicroservice inner-flow logic without a live
3
+ * gateway. Verifies that (a) the packaged kyc.bpmn deploys, (b) the inner
4
+ * flow drives to completion, (c) the aggregation produces the expected
5
+ * decision for representative customer IDs.
6
+ */
7
+ package com.example;
8
+
9
+ import static org.junit.jupiter.api.Assertions.assertEquals;
10
+ import static org.junit.jupiter.api.Assertions.assertTrue;
11
+
12
+ import io.github.jwulf.nano.bernd.EmbeddedEngine;
13
+ import java.io.InputStream;
14
+ import java.lang.reflect.Method;
15
+ import java.nio.charset.StandardCharsets;
16
+ import org.junit.jupiter.api.Test;
17
+
18
+ class KycInnerFlowTest {
19
+
20
+ @Test
21
+ void inner_flow_approves_a_low_risk_customer() throws Exception {
22
+ try (EmbeddedEngine engine = EmbeddedEngine.create()) {
23
+ engine.deploy(loadBpmn());
24
+ final String instanceKey = engine.createInstance("kyc");
25
+ assertTrue(!"0".equals(instanceKey), "engine rejected inner instance");
26
+
27
+ final KycMicroservice.KycContext ctx = new KycMicroservice.KycContext("customer-42");
28
+ invokeDriveInnerFlow(engine, ctx);
29
+ assertEquals("approved", ctx.aggregate());
30
+ assertTrue(engine.isCompleted(instanceKey), "inner instance should complete");
31
+ }
32
+ }
33
+
34
+ @Test
35
+ void inner_flow_flags_vip_for_manual_review() throws Exception {
36
+ try (EmbeddedEngine engine = EmbeddedEngine.create()) {
37
+ engine.deploy(loadBpmn());
38
+ engine.createInstance("kyc");
39
+ final KycMicroservice.KycContext ctx = new KycMicroservice.KycContext("vip-alice");
40
+ invokeDriveInnerFlow(engine, ctx);
41
+ assertEquals("manual_review", ctx.aggregate());
42
+ }
43
+ }
44
+
45
+ private static String loadBpmn() throws Exception {
46
+ try (InputStream in = KycInnerFlowTest.class.getResourceAsStream("/kyc/kyc.bpmn")) {
47
+ if (in == null) {
48
+ throw new IllegalStateException(
49
+ "test resource /kyc/kyc.bpmn not found on classpath — "
50
+ + "check that src/main/resources/kyc/kyc.bpmn is packaged");
51
+ }
52
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
53
+ }
54
+ }
55
+
56
+ private static void invokeDriveInnerFlow(
57
+ final EmbeddedEngine engine, final KycMicroservice.KycContext ctx) throws Exception {
58
+ final Method m =
59
+ KycMicroservice.class.getDeclaredMethod(
60
+ "driveInnerFlow", EmbeddedEngine.class, KycMicroservice.KycContext.class);
61
+ m.setAccessible(true);
62
+ m.invoke(null, engine, ctx);
63
+ }
64
+ }
@@ -0,0 +1,89 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
4
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ targetNamespace="http://example.com/onboarding"
6
+ id="onboarding_defs">
7
+
8
+ <!--
9
+ Customer onboarding — the OUTER process. Runs on the Nano IDE gateway
10
+ (models/ is auto-deployed on save). One service task 'verify_kyc' is
11
+ served by the embedded KYC microservice under ../microservice/, which
12
+ itself orchestrates the individual checks on its own in-process Bernd
13
+ engine.
14
+
15
+ Compliance owns kyc.bpmn (the inner flow); onboarding owns this file.
16
+ Both are inspectable BPMN — audit trail = deploy both, log both instance
17
+ keys.
18
+ -->
19
+
20
+ <bpmn:process id="onboarding" name="Customer Onboarding" isExecutable="true">
21
+ <bpmn:startEvent id="start" name="New customer">
22
+ <bpmn:outgoing>f_start</bpmn:outgoing>
23
+ </bpmn:startEvent>
24
+
25
+ <bpmn:serviceTask id="verify_kyc" name="Verify KYC">
26
+ <bpmn:extensionElements>
27
+ <zeebe:taskDefinition type="verify_kyc" />
28
+ </bpmn:extensionElements>
29
+ <bpmn:incoming>f_start</bpmn:incoming>
30
+ <bpmn:outgoing>f_to_gate</bpmn:outgoing>
31
+ </bpmn:serviceTask>
32
+
33
+ <bpmn:exclusiveGateway id="kyc_gate" name="KYC decision">
34
+ <bpmn:incoming>f_to_gate</bpmn:incoming>
35
+ <bpmn:outgoing>f_approved</bpmn:outgoing>
36
+ <bpmn:outgoing>f_manual</bpmn:outgoing>
37
+ <bpmn:outgoing>f_rejected</bpmn:outgoing>
38
+ </bpmn:exclusiveGateway>
39
+
40
+ <bpmn:serviceTask id="activate_account" name="Activate account">
41
+ <bpmn:extensionElements>
42
+ <zeebe:taskDefinition type="activate_account" />
43
+ </bpmn:extensionElements>
44
+ <bpmn:incoming>f_approved</bpmn:incoming>
45
+ <bpmn:outgoing>f_activated</bpmn:outgoing>
46
+ </bpmn:serviceTask>
47
+
48
+ <bpmn:serviceTask id="queue_for_ops" name="Queue for manual review">
49
+ <bpmn:extensionElements>
50
+ <zeebe:taskDefinition type="queue_for_ops" />
51
+ </bpmn:extensionElements>
52
+ <bpmn:incoming>f_manual</bpmn:incoming>
53
+ <bpmn:outgoing>f_queued</bpmn:outgoing>
54
+ </bpmn:serviceTask>
55
+
56
+ <bpmn:serviceTask id="notify_rejection" name="Notify rejection">
57
+ <bpmn:extensionElements>
58
+ <zeebe:taskDefinition type="notify_rejection" />
59
+ </bpmn:extensionElements>
60
+ <bpmn:incoming>f_rejected</bpmn:incoming>
61
+ <bpmn:outgoing>f_notified</bpmn:outgoing>
62
+ </bpmn:serviceTask>
63
+
64
+ <bpmn:endEvent id="end_approved" name="Onboarded">
65
+ <bpmn:incoming>f_activated</bpmn:incoming>
66
+ </bpmn:endEvent>
67
+ <bpmn:endEvent id="end_manual" name="Manual review">
68
+ <bpmn:incoming>f_queued</bpmn:incoming>
69
+ </bpmn:endEvent>
70
+ <bpmn:endEvent id="end_rejected" name="Rejected">
71
+ <bpmn:incoming>f_notified</bpmn:incoming>
72
+ </bpmn:endEvent>
73
+
74
+ <bpmn:sequenceFlow id="f_start" sourceRef="start" targetRef="verify_kyc" />
75
+ <bpmn:sequenceFlow id="f_to_gate" sourceRef="verify_kyc" targetRef="kyc_gate" />
76
+ <bpmn:sequenceFlow id="f_approved" name="approved" sourceRef="kyc_gate" targetRef="activate_account">
77
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=decision = "approved"</bpmn:conditionExpression>
78
+ </bpmn:sequenceFlow>
79
+ <bpmn:sequenceFlow id="f_manual" name="manual review" sourceRef="kyc_gate" targetRef="queue_for_ops">
80
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=decision = "manual_review"</bpmn:conditionExpression>
81
+ </bpmn:sequenceFlow>
82
+ <bpmn:sequenceFlow id="f_rejected" name="rejected" sourceRef="kyc_gate" targetRef="notify_rejection">
83
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=decision = "rejected"</bpmn:conditionExpression>
84
+ </bpmn:sequenceFlow>
85
+ <bpmn:sequenceFlow id="f_activated" sourceRef="activate_account" targetRef="end_approved" />
86
+ <bpmn:sequenceFlow id="f_queued" sourceRef="queue_for_ops" targetRef="end_manual" />
87
+ <bpmn:sequenceFlow id="f_notified" sourceRef="notify_rejection" targetRef="end_rejected" />
88
+ </bpmn:process>
89
+ </bpmn:definitions>