feat(wcs): add transport order state machine
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
package com.ruoyi.wcs.domain.task;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainEvent;
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TransportOrder {
|
||||
private final String orderId;
|
||||
private final String externalRequestId;
|
||||
private final TransportOrderType type;
|
||||
private final String loadId;
|
||||
private final String sourceCode;
|
||||
private final String destinationCode;
|
||||
private final int priority;
|
||||
private final Instant createdAt;
|
||||
private final List<DomainEvent> uncommittedEvents = new ArrayList<DomainEvent>();
|
||||
|
||||
private TransportOrderState state;
|
||||
private String workflowId;
|
||||
private String resultCode;
|
||||
private String stateReason;
|
||||
private long stateVersion;
|
||||
private long eventSequence;
|
||||
private Instant updatedAt;
|
||||
|
||||
private TransportOrder(String orderId, String externalRequestId,
|
||||
TransportOrderType type, String loadId,
|
||||
String sourceCode, String destinationCode,
|
||||
int priority, Instant createdAt) {
|
||||
this.orderId = requireText(orderId, "orderId");
|
||||
this.externalRequestId = requireText(externalRequestId, "externalRequestId");
|
||||
this.type = Objects.requireNonNull(type, "type");
|
||||
this.loadId = loadId;
|
||||
this.sourceCode = requireText(sourceCode, "sourceCode");
|
||||
this.destinationCode = requireText(destinationCode, "destinationCode");
|
||||
this.priority = priority;
|
||||
this.createdAt = Objects.requireNonNull(createdAt, "createdAt");
|
||||
this.updatedAt = createdAt;
|
||||
this.state = TransportOrderState.ACCEPTED;
|
||||
addEvent("OrderAccepted", createdAt);
|
||||
}
|
||||
|
||||
public static TransportOrder accept(String orderId, String externalRequestId,
|
||||
TransportOrderType type, String loadId,
|
||||
String sourceCode, String destinationCode,
|
||||
int priority, Instant createdAt) {
|
||||
return new TransportOrder(orderId, externalRequestId, type, loadId,
|
||||
sourceCode, destinationCode, priority, createdAt);
|
||||
}
|
||||
|
||||
public void plan(String workflowId) {
|
||||
requireState(TransportOrderState.ACCEPTED);
|
||||
this.workflowId = requireText(workflowId, "workflowId");
|
||||
transitionTo(TransportOrderState.PLANNED, "OrderPlanned");
|
||||
}
|
||||
|
||||
public void reject(String code, String reason) {
|
||||
requireState(TransportOrderState.ACCEPTED);
|
||||
this.resultCode = requireText(code, "code");
|
||||
this.stateReason = requireText(reason, "reason");
|
||||
transitionTo(TransportOrderState.REJECTED, "OrderRejected");
|
||||
}
|
||||
|
||||
public void start() {
|
||||
requireState(TransportOrderState.PLANNED);
|
||||
transitionTo(TransportOrderState.EXECUTING, "OrderStarted");
|
||||
}
|
||||
|
||||
public void suspend(String reason) {
|
||||
requireState(TransportOrderState.ACCEPTED, TransportOrderState.PLANNED,
|
||||
TransportOrderState.EXECUTING);
|
||||
this.stateReason = requireText(reason, "reason");
|
||||
transitionTo(TransportOrderState.SUSPENDED, "OrderSuspended");
|
||||
}
|
||||
|
||||
public void resumeToPlanned() {
|
||||
requireState(TransportOrderState.SUSPENDED);
|
||||
this.stateReason = null;
|
||||
transitionTo(TransportOrderState.PLANNED, "OrderResumedToPlanned");
|
||||
}
|
||||
|
||||
public void resumeToExecuting() {
|
||||
requireState(TransportOrderState.SUSPENDED);
|
||||
this.stateReason = null;
|
||||
transitionTo(TransportOrderState.EXECUTING, "OrderResumedToExecuting");
|
||||
}
|
||||
|
||||
public void requestCancel() {
|
||||
requireState(TransportOrderState.EXECUTING, TransportOrderState.SUSPENDED);
|
||||
transitionTo(TransportOrderState.CANCEL_REQUESTED, "OrderCancelRequested");
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
requireState(TransportOrderState.ACCEPTED, TransportOrderState.PLANNED,
|
||||
TransportOrderState.CANCEL_REQUESTED);
|
||||
transitionTo(TransportOrderState.CANCELLED, "OrderCancelled");
|
||||
}
|
||||
|
||||
public void complete() {
|
||||
requireState(TransportOrderState.EXECUTING);
|
||||
transitionTo(TransportOrderState.COMPLETED, "OrderCompleted");
|
||||
}
|
||||
|
||||
public void fail(String reason) {
|
||||
if (isTerminal()) {
|
||||
throw new DomainRuleViolation("terminal order cannot transition from " + state);
|
||||
}
|
||||
this.stateReason = requireText(reason, "reason");
|
||||
transitionTo(TransportOrderState.FAILED, "OrderFailed");
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getExternalRequestId() {
|
||||
return externalRequestId;
|
||||
}
|
||||
|
||||
public TransportOrderType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getLoadId() {
|
||||
return loadId;
|
||||
}
|
||||
|
||||
public String getSourceCode() {
|
||||
return sourceCode;
|
||||
}
|
||||
|
||||
public String getDestinationCode() {
|
||||
return destinationCode;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public TransportOrderState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getWorkflowId() {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
public String getResultCode() {
|
||||
return resultCode;
|
||||
}
|
||||
|
||||
public String getStateReason() {
|
||||
return stateReason;
|
||||
}
|
||||
|
||||
public long getStateVersion() {
|
||||
return stateVersion;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public List<DomainEvent> getUncommittedEvents() {
|
||||
return Collections.unmodifiableList(new ArrayList<DomainEvent>(uncommittedEvents));
|
||||
}
|
||||
|
||||
public void clearUncommittedEvents() {
|
||||
uncommittedEvents.clear();
|
||||
}
|
||||
|
||||
private void transitionTo(TransportOrderState next, String eventType) {
|
||||
this.state = next;
|
||||
this.stateVersion++;
|
||||
this.updatedAt = Instant.now();
|
||||
addEvent(eventType, updatedAt);
|
||||
}
|
||||
|
||||
private void addEvent(String eventType, Instant occurredAt) {
|
||||
eventSequence++;
|
||||
uncommittedEvents.add(new DomainEvent(
|
||||
orderId + ":" + eventSequence,
|
||||
orderId,
|
||||
eventSequence,
|
||||
occurredAt,
|
||||
eventType));
|
||||
}
|
||||
|
||||
private void requireState(TransportOrderState... allowed) {
|
||||
for (TransportOrderState candidate : allowed) {
|
||||
if (state == candidate) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new DomainRuleViolation("illegal order transition from " + state);
|
||||
}
|
||||
|
||||
private boolean isTerminal() {
|
||||
return state == TransportOrderState.REJECTED
|
||||
|| state == TransportOrderState.COMPLETED
|
||||
|| state == TransportOrderState.CANCELLED
|
||||
|| state == TransportOrderState.FAILED;
|
||||
}
|
||||
|
||||
private static String requireText(String value, String field) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.wcs.domain.task;
|
||||
|
||||
public enum TransportOrderState {
|
||||
ACCEPTED,
|
||||
PLANNED,
|
||||
EXECUTING,
|
||||
SUSPENDED,
|
||||
CANCEL_REQUESTED,
|
||||
REJECTED,
|
||||
COMPLETED,
|
||||
CANCELLED,
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.wcs.domain.task;
|
||||
|
||||
public enum TransportOrderType {
|
||||
INBOUND,
|
||||
OUTBOUND,
|
||||
RELOCATION,
|
||||
INVENTORY,
|
||||
CHARGING
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ruoyi.wcs.domain.task;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class TransportOrderTest {
|
||||
|
||||
@Test
|
||||
void completesOnlyThroughPlannedAndExecuting() {
|
||||
TransportOrder order = order();
|
||||
|
||||
order.plan("WF-1");
|
||||
order.start();
|
||||
order.complete();
|
||||
|
||||
assertEquals(TransportOrderState.COMPLETED, order.getState());
|
||||
assertEquals(4, order.getUncommittedEvents().size());
|
||||
assertEquals("OrderCompleted", order.getUncommittedEvents().get(3).getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotCompleteBeforeExecutionStarts() {
|
||||
TransportOrder order = order();
|
||||
order.plan("WF-1");
|
||||
|
||||
assertThrows(DomainRuleViolation.class, order::complete);
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalOrderCannotResume() {
|
||||
TransportOrder order = order();
|
||||
order.plan("WF-1");
|
||||
order.cancel();
|
||||
|
||||
assertThrows(DomainRuleViolation.class, order::resumeToPlanned);
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulTransitionsIncrementVersionAndEventSequenceOnce() {
|
||||
TransportOrder order = order();
|
||||
order.plan("WF-1");
|
||||
order.start();
|
||||
order.suspend("PLC snapshot stale");
|
||||
order.resumeToExecuting();
|
||||
|
||||
assertEquals(4L, order.getStateVersion());
|
||||
assertEquals(5L, order.getUncommittedEvents().get(4).getSequence());
|
||||
|
||||
assertThrows(DomainRuleViolation.class, order::resumeToPlanned);
|
||||
assertEquals(4L, order.getStateVersion());
|
||||
assertEquals(5, order.getUncommittedEvents().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runningCancellationRequiresTheExplicitConvergenceState() {
|
||||
TransportOrder order = order();
|
||||
order.plan("WF-1");
|
||||
order.start();
|
||||
|
||||
assertThrows(DomainRuleViolation.class, order::cancel);
|
||||
order.requestCancel();
|
||||
order.cancel();
|
||||
|
||||
assertEquals(TransportOrderState.CANCELLED, order.getState());
|
||||
}
|
||||
|
||||
private TransportOrder order() {
|
||||
return TransportOrder.accept(
|
||||
"ORDER-1",
|
||||
"REQ-1",
|
||||
TransportOrderType.INBOUND,
|
||||
"LOAD-1",
|
||||
"PORT-1",
|
||||
"SPACE-1",
|
||||
50,
|
||||
Instant.parse("2026-08-04T00:00:00Z"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user