diff --git a/javaTest/pom.xml b/javaTest/pom.xml
new file mode 100644
index 0000000..9385f63
--- /dev/null
+++ b/javaTest/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ com.ft546.wcs
+ wcs-dispatch-traffic-demo
+ 1.0.0-SNAPSHOT
+ FT546 WCS Dispatch and Traffic Demo
+
+
+ UTF-8
+ 1.8
+ 1.8
+ 5.10.2
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 8
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.ft546.wcs.demo.DemoApplication
+
+
+
+
+
diff --git a/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchQueueEntry.java b/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchQueueEntry.java
new file mode 100644
index 0000000..917fb35
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchQueueEntry.java
@@ -0,0 +1,125 @@
+package com.ft546.wcs.demo.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class DispatchQueueEntry {
+ public enum QueueType {
+ BUSINESS,
+ AVOIDANCE
+ }
+
+ private final String queueId;
+ private final String taskId;
+ private final String vehicleId;
+ private final QueueType queueType;
+ private final String startSite;
+ private final String endSite;
+ private final int priority;
+ private int issued;
+ private int executionStatus;
+ private List path;
+
+ private DispatchQueueEntry(
+ String queueId,
+ String taskId,
+ String vehicleId,
+ QueueType queueType,
+ String startSite,
+ String endSite,
+ int priority
+ ) {
+ this.queueId = queueId;
+ this.taskId = taskId;
+ this.vehicleId = vehicleId;
+ this.queueType = queueType;
+ this.startSite = startSite;
+ this.endSite = endSite;
+ this.priority = priority;
+ this.issued = 0;
+ this.executionStatus = 1;
+ this.path = Collections.emptyList();
+ }
+
+ public static DispatchQueueEntry business(DispatchTask task, String vehicleId) {
+ if (task == null) {
+ throw new IllegalArgumentException("task must not be null");
+ }
+ if (vehicleId == null || vehicleId.trim().isEmpty()) {
+ throw new IllegalArgumentException("vehicle id must not be blank");
+ }
+ return new DispatchQueueEntry(
+ task.getTaskId(),
+ task.getTaskId(),
+ vehicleId,
+ QueueType.BUSINESS,
+ task.getSourceSite(),
+ task.getTargetSite(),
+ task.getPriority()
+ );
+ }
+
+ public void markIssued(List issuedPath) {
+ if (issued != 0) {
+ throw new IllegalStateException("queue has already been issued");
+ }
+ if (issuedPath == null || issuedPath.isEmpty()) {
+ throw new IllegalArgumentException("issued path must not be empty");
+ }
+ this.path = Collections.unmodifiableList(new ArrayList(issuedPath));
+ this.issued = 1;
+ this.executionStatus = 3;
+ }
+
+ public void markCompleted() {
+ if (issued == 0) {
+ throw new IllegalStateException("unissued queue cannot complete");
+ }
+ this.executionStatus = 4;
+ }
+
+ public String getQueueId() {
+ return queueId;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public String getVehicleId() {
+ return vehicleId;
+ }
+
+ public QueueType getQueueType() {
+ return queueType;
+ }
+
+ public String getStartSite() {
+ return startSite;
+ }
+
+ public String getEndSite() {
+ return endSite;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public int getIssued() {
+ return issued;
+ }
+
+ public int getExecutionStatus() {
+ return executionStatus;
+ }
+
+ public boolean isIssued() {
+ return issued == 1;
+ }
+
+ public List getPath() {
+ return path;
+ }
+}
diff --git a/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchTask.java b/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchTask.java
new file mode 100644
index 0000000..961a874
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/demo/model/DispatchTask.java
@@ -0,0 +1,69 @@
+package com.ft546.wcs.demo.model;
+
+public final class DispatchTask {
+ public enum TaskType {
+ INBOUND,
+ OUTBOUND,
+ RELOCATION
+ }
+
+ private final String taskId;
+ private final TaskType type;
+ private final String sourceSite;
+ private final String targetSite;
+ private final int floor;
+ private final int priority;
+
+ public DispatchTask(
+ String taskId,
+ TaskType type,
+ String sourceSite,
+ String targetSite,
+ int floor,
+ int priority
+ ) {
+ this.taskId = requireText(taskId, "task id");
+ if (type == null) {
+ throw new IllegalArgumentException("task type must not be null");
+ }
+ this.type = type;
+ this.sourceSite = requireText(sourceSite, "source site");
+ this.targetSite = requireText(targetSite, "target site");
+ if (floor <= 0) {
+ throw new IllegalArgumentException("floor must be positive");
+ }
+ this.floor = floor;
+ this.priority = priority;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public TaskType getType() {
+ return type;
+ }
+
+ public String getSourceSite() {
+ return sourceSite;
+ }
+
+ public String getTargetSite() {
+ return targetSite;
+ }
+
+ public int getFloor() {
+ return floor;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ private static String requireText(String value, String name) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalArgumentException(name + " must not be blank");
+ }
+ return value;
+ }
+}
diff --git a/javaTest/src/main/java/com/ft546/wcs/demo/model/Site.java b/javaTest/src/main/java/com/ft546/wcs/demo/model/Site.java
new file mode 100644
index 0000000..ac6e060
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/demo/model/Site.java
@@ -0,0 +1,66 @@
+package com.ft546.wcs.demo.model;
+
+import java.util.Objects;
+
+public final class Site {
+ private final String id;
+ private final int floor;
+ private final int x;
+ private final int y;
+ private final String channel;
+
+ public Site(String id, int floor, int x, int y, String channel) {
+ this.id = requireText(id, "site id");
+ if (floor <= 0) {
+ throw new IllegalArgumentException("floor must be positive");
+ }
+ this.floor = floor;
+ this.x = x;
+ this.y = y;
+ this.channel = requireText(channel, "channel");
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public int getFloor() {
+ return floor;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public String getChannel() {
+ return channel;
+ }
+
+ private static String requireText(String value, String name) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalArgumentException(name + " must not be blank");
+ }
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Site)) {
+ return false;
+ }
+ Site site = (Site) other;
+ return id.equals(site.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/javaTest/src/main/java/com/ft546/wcs/demo/model/VehicleSnapshot.java b/javaTest/src/main/java/com/ft546/wcs/demo/model/VehicleSnapshot.java
new file mode 100644
index 0000000..af7960c
--- /dev/null
+++ b/javaTest/src/main/java/com/ft546/wcs/demo/model/VehicleSnapshot.java
@@ -0,0 +1,98 @@
+package com.ft546.wcs.demo.model;
+
+public final class VehicleSnapshot {
+ private final String vehicleId;
+ private final int floor;
+ private final String currentSite;
+ private final int workMode;
+ private final int status;
+ private final int battery;
+ private final boolean connected;
+ private final boolean allowSend;
+ private final boolean cargo;
+
+ public VehicleSnapshot(
+ String vehicleId,
+ int floor,
+ String currentSite,
+ int workMode,
+ int status,
+ int battery,
+ boolean connected,
+ boolean allowSend,
+ boolean cargo
+ ) {
+ this.vehicleId = requireText(vehicleId, "vehicle id");
+ if (floor <= 0) {
+ throw new IllegalArgumentException("floor must be positive");
+ }
+ this.floor = floor;
+ this.currentSite = requireText(currentSite, "current site");
+ this.workMode = workMode;
+ this.status = status;
+ if (battery < 0 || battery > 100) {
+ throw new IllegalArgumentException("battery must be between 0 and 100");
+ }
+ this.battery = battery;
+ this.connected = connected;
+ this.allowSend = allowSend;
+ this.cargo = cargo;
+ }
+
+ public String getVehicleId() {
+ return vehicleId;
+ }
+
+ public int getFloor() {
+ return floor;
+ }
+
+ public String getCurrentSite() {
+ return currentSite;
+ }
+
+ public int getWorkMode() {
+ return workMode;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public int getBattery() {
+ return battery;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public boolean isAllowSend() {
+ return allowSend;
+ }
+
+ public boolean isCargo() {
+ return cargo;
+ }
+
+ public VehicleSnapshot at(String siteId, int newFloor) {
+ return new VehicleSnapshot(
+ vehicleId,
+ newFloor,
+ siteId,
+ workMode,
+ status,
+ battery,
+ connected,
+ allowSend,
+ cargo
+ );
+ }
+
+ private static String requireText(String value, String name) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalArgumentException(name + " must not be blank");
+ }
+ return value;
+ }
+}
diff --git a/javaTest/src/test/java/com/ft546/wcs/demo/QueueLifecycleTest.java b/javaTest/src/test/java/com/ft546/wcs/demo/QueueLifecycleTest.java
new file mode 100644
index 0000000..53285f0
--- /dev/null
+++ b/javaTest/src/test/java/com/ft546/wcs/demo/QueueLifecycleTest.java
@@ -0,0 +1,80 @@
+package com.ft546.wcs.demo;
+
+import com.ft546.wcs.demo.model.DispatchQueueEntry;
+import com.ft546.wcs.demo.model.DispatchTask;
+import com.ft546.wcs.demo.model.Site;
+import com.ft546.wcs.demo.model.VehicleSnapshot;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class QueueLifecycleTest {
+
+ @Test
+ void queueUsesLegacyLifecycleCodes() {
+ DispatchTask task = new DispatchTask(
+ "T-1",
+ DispatchTask.TaskType.OUTBOUND,
+ "A",
+ "B",
+ 1,
+ 2
+ );
+ DispatchQueueEntry queue = DispatchQueueEntry.business(task, "1");
+
+ assertEquals(0, queue.getIssued());
+ assertEquals(1, queue.getExecutionStatus());
+
+ queue.markIssued(Arrays.asList("A", "B"));
+
+ assertEquals(1, queue.getIssued());
+ assertEquals(3, queue.getExecutionStatus());
+
+ queue.markCompleted();
+
+ assertEquals(4, queue.getExecutionStatus());
+ }
+
+ @Test
+ void queueCopiesPathAndRejectsCompletionBeforeIssue() {
+ DispatchTask task = new DispatchTask(
+ "T-2",
+ DispatchTask.TaskType.INBOUND,
+ "A",
+ "B",
+ 1,
+ 2
+ );
+ DispatchQueueEntry queue = DispatchQueueEntry.business(task, "1");
+ List path = new java.util.ArrayList(Arrays.asList("A", "B"));
+
+ assertThrows(IllegalStateException.class, queue::markCompleted);
+
+ queue.markIssued(path);
+ path.add("C");
+
+ assertEquals(Arrays.asList("A", "B"), queue.getPath());
+ assertThrows(UnsupportedOperationException.class, () -> queue.getPath().add("D"));
+ }
+
+ @Test
+ void domainInputsRejectInvalidValues() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Site("", 1, 0, 0, "C1"));
+ assertThrows(IllegalArgumentException.class,
+ () -> new VehicleSnapshot("1", 1, "A", 5, 1, 101, true, true, false));
+ assertThrows(IllegalArgumentException.class,
+ () -> new DispatchTask("", DispatchTask.TaskType.OUTBOUND, "A", "B", 1, 2));
+
+ VehicleSnapshot snapshot = new VehicleSnapshot(
+ "1", 1, "A", 5, 1, 80, true, true, false
+ );
+ assertTrue(snapshot.isConnected());
+ assertEquals("1", snapshot.getVehicleId());
+ }
+}