feat(wcs): add idempotent transport order use cases
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user