From 3e704d0b10a012d27f2fb3928cde9d8527e7672a Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Tue, 11 Aug 2026 14:46:36 +0800 Subject: [PATCH] feat(ft546): adapt existing WMS contract to WCS core --- .../api/controller/Wcs2WmsController.java | 37 ++- .../api/service/impl/ApiServiceImpl.java | 35 +-- .../ft546/wms/Ft546WmsAdapterService.java | 104 +++++++ .../wms/Ft546WmsNotificationGateway.java | 68 +++++ .../ft546/wms/Ft546WmsOrderMapper.java | 82 ++++++ .../ft546/wms/Ft546WmsAdapterServiceTest.java | 265 ++++++++++++++++++ 6 files changed, 556 insertions(+), 35 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java index 86df745..4da08d4 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java @@ -5,6 +5,8 @@ import com.ruoyi.api.domain.wcs2wms.JobParam; import com.ruoyi.api.domain.wcs2wms.ReceiveJob; import com.ruoyi.api.service.IApiService; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.project.ft546.config.Ft546WcsProperties; +import com.ruoyi.project.ft546.wms.Ft546WmsAdapterService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.bind.annotation.PostMapping; @@ -15,9 +17,18 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/wcs2Wms/api") public class Wcs2WmsController { + private final IApiService apiService; + private final Ft546WmsAdapterService standardAdapter; + private final Ft546WcsProperties properties; @Autowired - private IApiService apiService; + public Wcs2WmsController(IApiService apiService, + Ft546WmsAdapterService standardAdapter, + Ft546WcsProperties properties) { + this.apiService = apiService; + this.standardAdapter = standardAdapter; + this.properties = properties; + } /** * 接收出库任务 @@ -27,7 +38,10 @@ public class Wcs2WmsController { */ @PostMapping("/receiveOutMydoJob") public AjaxResult receiveOutMydoJob(@RequestBody ReceiveJob receiveOutJob) { - return apiService.receiveOutMydoJob(receiveOutJob); + return isStandard() + ? AjaxResult.success( + standardAdapter.receiveOutMydoJob(receiveOutJob)) + : apiService.receiveOutMydoJob(receiveOutJob); } /** @@ -38,7 +52,10 @@ public class Wcs2WmsController { */ @PostMapping("/receiveInMydoJob") public AjaxResult receiveInMydoJob(@RequestBody ReceiveJob receiveInJob) { - return apiService.receiveInMydoJob(receiveInJob); + return isStandard() + ? AjaxResult.success( + standardAdapter.receiveInMydoJob(receiveInJob)) + : apiService.receiveInMydoJob(receiveInJob); } @@ -50,13 +67,23 @@ public class Wcs2WmsController { */ @PostMapping("/cancelMydoJob") public AjaxResult cancelMydoJob(@RequestBody JobParam jobParam) { - return apiService.cancelMydoJob(jobParam); + return isStandard() + ? AjaxResult.success(standardAdapter.cancelMydoJob( + jobParam == null ? null : jobParam.getTaskNo())) + : apiService.cancelMydoJob(jobParam); } /** Return a read-only snapshot of all S-RGV vehicles. */ @PostMapping("/tidyVehicleSnapshot") public AjaxResult tidyVehicleSnapshot() { - return apiService.tidyVehicleSnapshot(); + return isStandard() + ? AjaxResult.success(standardAdapter.tidyVehicleSnapshot()) + : apiService.tidyVehicleSnapshot(); + } + + private boolean isStandard() { + return properties.getRuntimeMode() + == Ft546WcsProperties.RuntimeMode.STANDARD; } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java index 5a5a65f..fddb0d2 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java @@ -27,6 +27,7 @@ import com.ruoyi.job.domain.MydoJob; import com.ruoyi.job.service.IMydoJobService; import com.ruoyi.queue.agv.domain.AgvQueue; import com.ruoyi.queue.agv.service.IAgvQueueService; +import com.ruoyi.project.ft546.wms.Ft546WmsAdapterService; import com.ruoyi.system.domain.AgvContainerTask; import com.ruoyi.system.service.IAgvContainerTaskService; import lombok.extern.slf4j.Slf4j; @@ -60,6 +61,8 @@ public class ApiServiceImpl implements IApiService { private IAgvContainerTaskService agvContainerTaskService; @Autowired private DeviceSrgvMapper deviceSrgvMapper; + @Autowired + private Ft546WmsAdapterService ft546WmsAdapterService; /** @@ -520,36 +523,8 @@ public class ApiServiceImpl implements IApiService { @Override public AjaxResult tidyVehicleSnapshot() { - List vehicles = deviceSrgvMapper.selectDeviceSrgvList(new DeviceSrgv()); - List snapshots = new ArrayList<>(); - for (DeviceSrgv vehicle : vehicles) { - boolean online = "1".equals(vehicle.getAgvStatus()); - boolean healthy = online && value(vehicle.getAlarm()) <= 1 && value(vehicle.getErrMsg()) == 0; - boolean idle = healthy && value(vehicle.getTaskNo()) == 0 - && value(vehicle.getLocked()) == 0 && value(vehicle.getChargeFlag()) == 0; - SrgvVehicleSnapshot snapshot = new SrgvVehicleSnapshot(); - snapshot.setGuid(vehicle.getGuid()); - snapshot.setDeviceId(vehicle.getDeviceId()); - snapshot.setDeviceName(vehicle.getDeviceName()); - snapshot.setAgvStatus(vehicle.getAgvStatus()); - snapshot.setOnline(online); - snapshot.setHealthy(healthy); - snapshot.setIdle(idle); - snapshot.setTaskNo(vehicle.getTaskNo()); - snapshot.setAlarm(vehicle.getAlarm()); - snapshot.setErrMsg(vehicle.getErrMsg()); - snapshot.setChargeFlag(vehicle.getChargeFlag()); - snapshot.setLocked(vehicle.getLocked()); - snapshot.setBattery(vehicle.getBattery()); - snapshot.setCurrSite(vehicle.getCurrSite()); - snapshot.setCurrFloor(vehicle.getCurrFloor()); - snapshots.add(snapshot); - } - return AjaxResult.success(snapshots); - } - - private int value(Integer number) { - return number == null ? 0 : number; + return AjaxResult.success(ft546WmsAdapterService + .tidyVehicleSnapshot()); } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java new file mode 100644 index 0000000..0dabe5a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java @@ -0,0 +1,104 @@ +package com.ruoyi.project.ft546.wms; + +import com.ruoyi.api.domain.wcs2wms.ReceiveJob; +import com.ruoyi.api.domain.wcs2wms.SrgvVehicleSnapshot; +import com.ruoyi.device.srgv.domain.DeviceSrgv; +import com.ruoyi.device.srgv.mapper.DeviceSrgvMapper; +import com.ruoyi.wcs.application.task.TransportOrderResult; +import com.ruoyi.wcs.port.in.TransportOrderUseCase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@Service +public class Ft546WmsAdapterService { + private final TransportOrderUseCase orderUseCase; + private final VehicleSnapshotProvider vehicleSnapshotProvider; + private final Ft546WmsOrderMapper mapper = new Ft546WmsOrderMapper(); + + @Autowired + public Ft546WmsAdapterService(TransportOrderUseCase orderUseCase, + DeviceSrgvMapper deviceMapper) { + this(orderUseCase, () -> snapshots(deviceMapper)); + } + + public Ft546WmsAdapterService(TransportOrderUseCase orderUseCase, + VehicleSnapshotProvider snapshotProvider) { + this.orderUseCase = Objects.requireNonNull(orderUseCase, + "orderUseCase"); + this.vehicleSnapshotProvider = Objects.requireNonNull( + snapshotProvider, "snapshotProvider"); + } + + public TransportOrderResult receiveOutMydoJob(ReceiveJob job) { + return orderUseCase.create(mapper.outbound(job)); + } + + public TransportOrderResult receiveInMydoJob(ReceiveJob job) { + return orderUseCase.create(mapper.inbound(job)); + } + + public TransportOrderResult cancelMydoJob(String taskNo) { + return orderUseCase.cancel(requireText(taskNo, "taskNo")); + } + + public List tidyVehicleSnapshot() { + List snapshots = Objects.requireNonNull( + vehicleSnapshotProvider.load(), "vehicle snapshots"); + return Collections.unmodifiableList(new ArrayList(snapshots)); + } + + private static List snapshots( + DeviceSrgvMapper mapper) { + List vehicles = Objects.requireNonNull(mapper, + "deviceMapper").selectDeviceSrgvList(new DeviceSrgv()); + List snapshots = + new ArrayList(); + for (DeviceSrgv vehicle : vehicles) { + boolean online = "1".equals(vehicle.getAgvStatus()); + boolean healthy = online && value(vehicle.getAlarm()) <= 1 + && value(vehicle.getErrMsg()) == 0; + boolean idle = healthy && value(vehicle.getTaskNo()) == 0 + && value(vehicle.getLocked()) == 0 + && value(vehicle.getChargeFlag()) == 0; + SrgvVehicleSnapshot snapshot = new SrgvVehicleSnapshot(); + snapshot.setGuid(vehicle.getGuid()); + snapshot.setDeviceId(vehicle.getDeviceId()); + snapshot.setDeviceName(vehicle.getDeviceName()); + snapshot.setAgvStatus(vehicle.getAgvStatus()); + snapshot.setOnline(online); + snapshot.setHealthy(healthy); + snapshot.setIdle(idle); + snapshot.setTaskNo(vehicle.getTaskNo()); + snapshot.setAlarm(vehicle.getAlarm()); + snapshot.setErrMsg(vehicle.getErrMsg()); + snapshot.setChargeFlag(vehicle.getChargeFlag()); + snapshot.setLocked(vehicle.getLocked()); + snapshot.setBattery(vehicle.getBattery()); + snapshot.setCurrSite(vehicle.getCurrSite()); + snapshot.setCurrFloor(vehicle.getCurrFloor()); + snapshots.add(snapshot); + } + return snapshots; + } + + private static int value(Integer number) { + return number == null ? 0 : number; + } + + private static String requireText(String value, String field) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return value.trim(); + } + + @FunctionalInterface + public interface VehicleSnapshotProvider { + List load(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java new file mode 100644 index 0000000..cc8739a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java @@ -0,0 +1,68 @@ +package com.ruoyi.project.ft546.wms; + +import cn.hutool.json.JSONUtil; +import com.ruoyi.api.domain.RequestLog; +import com.ruoyi.api.service.IRequestLogService; +import com.ruoyi.wcs.port.out.WmsNotification; +import com.ruoyi.wcs.port.out.WmsNotificationGateway; +import com.ruoyi.wcs.port.out.WmsNotificationReceipt; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import static com.ruoyi.common.Constant.JOB_FINISH_URL; + +@Component +public final class Ft546WmsNotificationGateway + implements WmsNotificationGateway { + private static final java.util.Set SUPPORTED_EVENTS = + new java.util.HashSet(Arrays.asList("OrderCompleted", + "OrderFailed", "OrderCancelled")); + private final IRequestLogService requestLogService; + + public Ft546WmsNotificationGateway( + IRequestLogService requestLogService) { + this.requestLogService = Objects.requireNonNull(requestLogService, + "requestLogService"); + } + + @Override + public WmsNotificationReceipt notify(WmsNotification notification) { + Objects.requireNonNull(notification, "notification"); + if (!SUPPORTED_EVENTS.contains(notification.getEventType())) { + return new WmsNotificationReceipt( + WmsNotificationReceipt.Status.PERMANENT_FAILURE, + "unsupported FT546 notification event"); + } + Map payload = new LinkedHashMap(); + payload.put("eventId", notification.getEventId()); + payload.put("jobId", context(notification, "jobId", + notification.getOrderId())); + payload.put("taskNo", notification.getExternalRequestId()); + payload.put("wmsType", context(notification, "inOrOut", "UNKNOWN")); + payload.put("eventType", notification.getEventType()); + payload.put("resultCode", notification.getResultCode()); + payload.put("resultMessage", notification.getResultMessage()); + RequestLog result = requestLogService.getRequestLog(JOB_FINISH_URL, + "WCS标准任务结果回传", JSONUtil.toJsonStr(payload)); + if (result != null && "1".equals(result.getIsSuccessful())) { + return new WmsNotificationReceipt( + WmsNotificationReceipt.Status.DELIVERED, + notification.getEventId()); + } + return new WmsNotificationReceipt( + WmsNotificationReceipt.Status.RETRYABLE_FAILURE, + result == null ? "callback returned no result" + : result.getErrorDetails()); + } + + private static String context(WmsNotification notification, String key, + String fallback) { + String value = notification.getContext().get(key); + return value == null || value.trim().isEmpty() + ? fallback : value.trim(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java new file mode 100644 index 0000000..a3f9a47 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java @@ -0,0 +1,82 @@ +package com.ruoyi.project.ft546.wms; + +import com.ruoyi.api.domain.wcs2wms.ReceiveJob; +import com.ruoyi.wcs.application.task.CreateTransportOrderCommand; +import com.ruoyi.wcs.domain.task.TransportOrderType; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public final class Ft546WmsOrderMapper { + public CreateTransportOrderCommand inbound(ReceiveJob job) { + ReceiveJob request = Objects.requireNonNull(job, "job"); + return command(request, TransportOrderType.INBOUND, + firstText(request.getEntranceId(), request.getExitId()), + requireText(request.getCargoSpaceId(), "cargoSpaceId")); + } + + public CreateTransportOrderCommand outbound(ReceiveJob job) { + ReceiveJob request = Objects.requireNonNull(job, "job"); + return command(request, TransportOrderType.OUTBOUND, + requireText(request.getCargoSpaceId(), "cargoSpaceId"), + firstText(request.getExitId(), + request.getTargetCargoSpaceId())); + } + + private static CreateTransportOrderCommand command( + ReceiveJob job, TransportOrderType type, String source, + String destination) { + String taskNo = requireText(job.getTaskNo(), "taskNo"); + Map attributes = new LinkedHashMap(); + put(attributes, "legacyTaskNo", taskNo); + put(attributes, "jobId", job.getJobId()); + put(attributes, "inOrOut", job.getInOrOut()); + put(attributes, "roadwayId", job.getRoadwayId()); + put(attributes, "classId", job.getClassId()); + put(attributes, "entranceId", job.getEntranceId()); + put(attributes, "exitId", job.getExitId()); + put(attributes, "cargoSpaceId", job.getCargoSpaceId()); + put(attributes, "targetCargoSpaceId", + job.getTargetCargoSpaceId()); + return new CreateTransportOrderCommand(taskNo, type, + textOrNull(job.getTrayCode()), source, destination, + priority(job.getPriority()), null, attributes); + } + + private static int priority(Long value) { + if (value == null) { + return 0; + } + if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + throw new IllegalArgumentException("priority is out of range"); + } + return value.intValue(); + } + + private static void put(Map target, String key, + String value) { + String text = textOrNull(value); + if (text != null) { + target.put(key, text); + } + } + + private static String firstText(String first, String second) { + String value = textOrNull(first); + return value != null ? value : requireText(second, "location"); + } + + private static String textOrNull(String value) { + return value == null || value.trim().isEmpty() + ? null : value.trim(); + } + + private static String requireText(String value, String field) { + String text = textOrNull(value); + if (text == null) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return text; + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java new file mode 100644 index 0000000..a6514b8 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java @@ -0,0 +1,265 @@ +package com.ruoyi.project.ft546.wms; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.ruoyi.api.controller.Wcs2WmsController; +import com.ruoyi.api.domain.RequestLog; +import com.ruoyi.api.domain.wcs2wms.JobParam; +import com.ruoyi.api.domain.wcs2wms.ReceiveJob; +import com.ruoyi.api.service.IApiService; +import com.ruoyi.api.service.impl.RequestLogServiceImpl; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.project.ft546.config.Ft546WcsProperties; +import com.ruoyi.wcs.application.task.CreateTransportOrderCommand; +import com.ruoyi.wcs.application.task.TransportOrderResult; +import com.ruoyi.wcs.domain.task.TransportOrder; +import com.ruoyi.wcs.domain.task.TransportOrderType; +import com.ruoyi.wcs.port.in.TransportOrderUseCase; +import com.ruoyi.wcs.port.out.WmsNotification; +import com.ruoyi.wcs.port.out.WmsNotificationReceipt; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class Ft546WmsAdapterServiceTest { + private static final Instant NOW = Instant.parse("2026-08-11T06:00:00Z"); + + @Test + void mapsExistingInboundRequestToCanonicalOrder() { + Ft546WmsOrderMapper mapper = new Ft546WmsOrderMapper(); + ReceiveJob job = job("TASK-1"); + job.setEntranceId("PORT-1"); + job.setCargoSpaceId("SPACE-9"); + + CreateTransportOrderCommand command = mapper.inbound(job); + + assertEquals("TASK-1", command.getExternalRequestId()); + assertEquals(TransportOrderType.INBOUND, command.getOrderType()); + assertEquals("PORT-1", command.getSourceCode()); + assertEquals("SPACE-9", command.getDestinationCode()); + assertEquals("TRAY-1", command.getLoadId()); + assertEquals("JOB-1", command.getAttributes().get("jobId")); + assertEquals("TASK-1", + command.getAttributes().get("legacyTaskNo")); + } + + @Test + void mapsExistingOutboundRequestToCanonicalOrder() { + Ft546WmsOrderMapper mapper = new Ft546WmsOrderMapper(); + ReceiveJob job = job("TASK-2"); + job.setCargoSpaceId("SPACE-9"); + job.setExitId("PORT-2"); + + CreateTransportOrderCommand command = mapper.outbound(job); + + assertEquals(TransportOrderType.OUTBOUND, command.getOrderType()); + assertEquals("SPACE-9", command.getSourceCode()); + assertEquals("PORT-2", command.getDestinationCode()); + } + + @Test + void duplicateWmsRequestReturnsTheSameOrder() { + FakeTransportOrderUseCase useCase = new FakeTransportOrderUseCase(); + Ft546WmsAdapterService service = new Ft546WmsAdapterService(useCase, + () -> Collections.singletonList("SNAPSHOT")); + ReceiveJob job = job("TASK-1"); + job.setEntranceId("PORT-1"); + job.setCargoSpaceId("SPACE-9"); + + TransportOrderResult first = service.receiveInMydoJob(job); + TransportOrderResult duplicate = service.receiveInMydoJob(job); + + assertEquals(first.getOrderId(), duplicate.getOrderId()); + assertEquals(2, useCase.createCommands.size()); + assertEquals("TASK-1", useCase.createCommands.get(1) + .getExternalRequestId()); + } + + @Test + void controllerUsesStandardAdapterOnlyInStandardMode() { + IApiService legacy = mock(IApiService.class); + AjaxResult legacyResult = AjaxResult.success("legacy"); + ReceiveJob job = job("TASK-1"); + job.setEntranceId("PORT-1"); + job.setCargoSpaceId("SPACE-9"); + when(legacy.receiveInMydoJob(job)).thenReturn(legacyResult); + FakeTransportOrderUseCase useCase = new FakeTransportOrderUseCase(); + Ft546WmsAdapterService standard = new Ft546WmsAdapterService(useCase, + () -> Collections.singletonList("SNAPSHOT")); + Ft546WcsProperties properties = new Ft546WcsProperties(); + Wcs2WmsController controller = new Wcs2WmsController(legacy, + standard, properties); + + assertSame(legacyResult, controller.receiveInMydoJob(job)); + verify(legacy).receiveInMydoJob(job); + + properties.setRuntimeMode(Ft546WcsProperties.RuntimeMode.STANDARD); + AjaxResult standardResult = controller.receiveInMydoJob(job); + + assertTrue(standardResult.isSuccess()); + assertTrue(standardResult.get(AjaxResult.DATA_TAG) + instanceof TransportOrderResult); + assertEquals(1, useCase.createCommands.size()); + } + + @Test + void controllerDelegatesCancelAndSnapshotWithoutLegacyWritesInStandard() { + IApiService legacy = mock(IApiService.class); + FakeTransportOrderUseCase useCase = new FakeTransportOrderUseCase(); + Ft546WmsAdapterService standard = new Ft546WmsAdapterService(useCase, + () -> Arrays.asList("S1", "S2")); + Ft546WcsProperties properties = new Ft546WcsProperties(); + properties.setRuntimeMode(Ft546WcsProperties.RuntimeMode.STANDARD); + Wcs2WmsController controller = new Wcs2WmsController(legacy, + standard, properties); + JobParam cancel = new JobParam("JOB-1", "TRAY-1", "TASK-1"); + + AjaxResult cancelResult = controller.cancelMydoJob(cancel); + AjaxResult snapshotResult = controller.tidyVehicleSnapshot(); + + assertTrue(cancelResult.isSuccess()); + assertEquals(Arrays.asList("S1", "S2"), + snapshotResult.get(AjaxResult.DATA_TAG)); + assertEquals(Collections.singletonList("TASK-1"), + useCase.cancelledExternalIds); + verify(legacy, never()).cancelMydoJob(any(JobParam.class)); + verify(legacy, never()).tidyVehicleSnapshot(); + } + + @Test + void notificationUsesEventIdAsLegacyCallbackIdempotencyKey() { + FakeRequestLogService requestLogs = new FakeRequestLogService(); + Ft546WmsNotificationGateway gateway = + new Ft546WmsNotificationGateway(requestLogs); + Map context = new LinkedHashMap(); + context.put("jobId", "JOB-1"); + context.put("inOrOut", "10"); + WmsNotification notification = new WmsNotification("EVENT-1", + "TASK-1", "ORDER-1", NOW, "OrderCompleted", "OK", + "completed", context); + + WmsNotificationReceipt receipt = gateway.notify(notification); + + assertEquals(WmsNotificationReceipt.Status.DELIVERED, + receipt.getStatus()); + JSONObject payload = JSONUtil.parseObj(requestLogs.requestBody); + assertEquals("EVENT-1", payload.getStr("eventId")); + assertEquals("JOB-1", payload.getStr("jobId")); + assertEquals("TASK-1", payload.getStr("taskNo")); + assertEquals("10", payload.getStr("wmsType")); + assertEquals("OrderCompleted", payload.getStr("eventType")); + } + + @Test + void notificationTransportFailureRemainsRetryable() { + FakeRequestLogService requestLogs = new FakeRequestLogService(); + requestLogs.success = false; + Ft546WmsNotificationGateway gateway = + new Ft546WmsNotificationGateway(requestLogs); + WmsNotification notification = new WmsNotification("EVENT-2", + "TASK-2", "ORDER-2", NOW, "OrderFailed", "FAILED", + "fault", Collections.emptyMap()); + + WmsNotificationReceipt receipt = gateway.notify(notification); + + assertEquals(WmsNotificationReceipt.Status.RETRYABLE_FAILURE, + receipt.getStatus()); + } + + private static ReceiveJob job(String taskNo) { + ReceiveJob job = new ReceiveJob(); + job.setTaskNo(taskNo); + job.setJobId("JOB-1"); + job.setTrayCode("TRAY-1"); + job.setInOrOut("10"); + job.setRoadwayId("R1"); + job.setClassId("7"); + job.setPriority(5L); + return job; + } + + private static final class FakeTransportOrderUseCase + implements TransportOrderUseCase { + private final Map orders = + new LinkedHashMap(); + private final List createCommands = + new ArrayList(); + private final List cancelledExternalIds = + new ArrayList(); + + @Override + public TransportOrderResult create(CreateTransportOrderCommand command) { + createCommands.add(command); + TransportOrderResult existing = orders.get( + command.getExternalRequestId()); + if (existing != null) { + return existing; + } + TransportOrder order = TransportOrder.accept( + "ORDER-" + command.getExternalRequestId(), + command.getExternalRequestId(), command.getOrderType(), + command.getLoadId(), command.getSourceCode(), + command.getDestinationCode(), command.getPriority(), NOW); + order.plan("WORKFLOW-" + command.getExternalRequestId()); + TransportOrderResult result = TransportOrderResult.from(order); + orders.put(command.getExternalRequestId(), result); + return result; + } + + @Override + public TransportOrderResult cancel(String externalRequestId) { + cancelledExternalIds.add(externalRequestId); + TransportOrderResult existing = orders.get(externalRequestId); + if (existing != null) { + return existing; + } + CreateTransportOrderCommand command = + new CreateTransportOrderCommand(externalRequestId, + TransportOrderType.INBOUND, null, "UNKNOWN-SOURCE", + "UNKNOWN-TARGET", 0, null, + Collections.emptyMap()); + return create(command); + } + + @Override + public TransportOrderResult suspend(String orderId, String reason) { + throw new UnsupportedOperationException(); + } + + @Override + public TransportOrderResult resume(String orderId) { + throw new UnsupportedOperationException(); + } + } + + private static final class FakeRequestLogService + extends RequestLogServiceImpl { + private boolean success = true; + private String requestBody; + + @Override + public RequestLog getRequestLog(String url, String endpoint, + String body) { + this.requestBody = body; + RequestLog log = new RequestLog(); + log.setIsSuccessful(success ? "1" : "0"); + log.setErrorDetails(success ? null : "injected failure"); + return log; + } + } +}