feat(wcs): add idempotent transport order use cases

This commit is contained in:
zgw
2026-08-10 15:15:29 +08:00
parent fd83225c75
commit f4ae3df382
10 changed files with 565 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
package com.ruoyi.wcs.application.task;
import com.ruoyi.wcs.domain.shared.DomainEvent;
import com.ruoyi.wcs.domain.shared.IdGenerator;
import com.ruoyi.wcs.domain.shared.WcsClock;
import com.ruoyi.wcs.domain.task.TaskStepType;
import com.ruoyi.wcs.domain.task.TransportOrder;
import com.ruoyi.wcs.domain.task.TransportOrderState;
import com.ruoyi.wcs.domain.task.TransportOrderType;
import com.ruoyi.wcs.domain.task.WorkflowDefinition;
import com.ruoyi.wcs.domain.task.WorkflowInstance;
import com.ruoyi.wcs.domain.task.WorkflowStepDefinition;
import com.ruoyi.wcs.port.out.DomainEventPublisher;
import com.ruoyi.wcs.port.out.TransportOrderRepository;
import com.ruoyi.wcs.port.out.WorkflowDefinitionProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TransportOrderApplicationServiceTest {
private InMemoryRepository repository;
private RecordingPublisher publisher;
private TransportOrderApplicationService service;
@BeforeEach
void setUp() {
repository = new InMemoryRepository();
publisher = new RecordingPublisher();
service = new TransportOrderApplicationService(
repository,
new SingleStepDefinitions(),
publisher,
new SequenceIdGenerator(),
new FixedClock());
}
@Test
void returnsTheExistingOrderForTheSameExternalRequest() {
TransportOrderResult first = service.create(command("REQ-1"));
TransportOrderResult second = service.create(command("REQ-1"));
assertEquals(first.getOrderId(), second.getOrderId());
assertEquals(1, repository.count());
assertEquals(2, publisher.events.size());
}
@Test
void createsAPlannedOrderAndItsWorkflowAtomically() {
TransportOrderResult result = service.create(command("REQ-2"));
assertEquals(TransportOrderState.PLANNED, result.getState());
assertEquals("PORT-1", result.getSourceCode());
assertEquals("SPACE-9", result.getDestinationCode());
assertEquals(1, repository.workflows.size());
assertEquals("OrderAccepted", publisher.events.get(0).getType());
assertEquals("OrderPlanned", publisher.events.get(1).getType());
}
@Test
void cancelBeforeExecutionProducesOneCancelledTransition() {
service.create(command("REQ-3"));
TransportOrderResult cancelled = service.cancel("REQ-3");
assertEquals(TransportOrderState.CANCELLED, cancelled.getState());
assertEquals("OrderCancelled", publisher.events.get(2).getType());
}
private CreateTransportOrderCommand command(String requestId) {
return new CreateTransportOrderCommand(
requestId,
TransportOrderType.INBOUND,
"LOAD-1",
"PORT-1",
"SPACE-9",
50,
null,
Collections.<String, String>emptyMap());
}
private static final class FixedClock implements WcsClock {
@Override
public Instant now() {
return Instant.parse("2026-08-04T00:00:00Z");
}
}
private static final class SequenceIdGenerator implements IdGenerator {
private int next = 1;
@Override
public String nextId() {
return "ID-" + next++;
}
}
private static final class SingleStepDefinitions implements WorkflowDefinitionProvider {
@Override
public WorkflowDefinition definitionFor(TransportOrderType orderType) {
return new WorkflowDefinition(orderType, Collections.singletonList(
new WorkflowStepDefinition("MOVE", TaskStepType.MOVE,
true, true, Collections.<String>emptyList())));
}
}
private static final class RecordingPublisher implements DomainEventPublisher {
private final List<DomainEvent> events = new ArrayList<DomainEvent>();
@Override
public void publish(List<DomainEvent> domainEvents) {
events.addAll(domainEvents);
}
}
private static final class InMemoryRepository implements TransportOrderRepository {
private final Map<String, TransportOrder> orders =
new LinkedHashMap<String, TransportOrder>();
private final Map<String, WorkflowInstance> workflows =
new LinkedHashMap<String, WorkflowInstance>();
@Override
public TransportOrder findByExternalRequestId(String externalRequestId) {
return orders.get(externalRequestId);
}
@Override
public TransportOrder findByOrderId(String orderId) {
for (TransportOrder order : orders.values()) {
if (order.getOrderId().equals(orderId)) {
return order;
}
}
return null;
}
@Override
public void save(TransportOrder order, WorkflowInstance workflow) {
orders.put(order.getExternalRequestId(), order);
if (workflow != null) {
workflows.put(workflow.getWorkflowId(), workflow);
}
}
@Override
public void save(TransportOrder order) {
orders.put(order.getExternalRequestId(), order);
}
int count() {
return orders.size();
}
}
}