diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java index ea792ea46..490bba307 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java @@ -111,14 +111,26 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { .uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode) .header("X-Internal-Service-Token", internalToken) .retrieve().onStatus(HttpStatusCode::isError, (request, result) -> { - throw new IllegalArgumentException("Warehouse rejected pallet code: HTTP " + result.getStatusCode()); + throw palletLookupFailure(result.getStatusCode()); }).body(WarehousePalletResponse.class); - if (response == null || response.data() == null) throw new IllegalArgumentException("Warehouse returned an empty pallet response"); + if (response != null && "A0404".equals(response.code())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Warehouse pallet was not found"); + } + if (response == null || response.data() == null) { + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, + "Warehouse pallet service returned no usable response"); + } return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().warehouseId(), response.data().locationId(), response.data().status(), response.data().fbaWarehouseCode(), response.data().version()); } + private ResponseStatusException palletLookupFailure(HttpStatusCode status) { + return new ResponseStatusException(status.value() == 404 ? HttpStatus.NOT_FOUND + : (status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE), + status.value() == 404 ? "Warehouse pallet was not found" : "Warehouse pallet lookup failed"); + } + @Override public WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) { WarehouseLocationResponse response = client.get() @@ -173,7 +185,7 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { return response.data(); } - private record WarehousePalletResponse(WarehousePalletData data) {} + private record WarehousePalletResponse(String code, String message, WarehousePalletData data) {} private record WarehousePalletData(@JsonAlias("palletId") long id, String palletCode, long warehouseId, long locationId, String status, String fbaWarehouseCode, int version) {} private record ReserveEnvelope(ReserveData data) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java index 4b4154ddf..dbdfa5044 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java @@ -6,8 +6,10 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatusCode; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.server.ResponseStatusException; import java.util.LinkedHashMap; import java.util.Map; @@ -31,12 +33,24 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway { ResolveEnvelope response = client.get().uri("/internal/v1/warehouse-pallets/resolve/{code}", palletCode) .header("X-Internal-Service-Token", internalToken).retrieve() .onStatus(HttpStatusCode::isError, (request, result) -> { - throw new IllegalArgumentException("Warehouse rejected pallet scan: HTTP " + result.getStatusCode()); + throw palletLookupFailure(result.getStatusCode()); }).body(ResolveEnvelope.class); - if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned no pallet"); + if (response != null && "A0404".equals(response.code())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Warehouse pallet was not found"); + } + if (response == null || response.data() == null) { + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, + "Warehouse pallet service returned no usable response"); + } return response.data(); } + private ResponseStatusException palletLookupFailure(HttpStatusCode status) { + return new ResponseStatusException(status.value() == 404 ? HttpStatus.NOT_FOUND + : (status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE), + status.value() == 404 ? "Warehouse pallet was not found" : "Warehouse pallet lookup failed"); + } + @Override public LocationReference resolveLocation(long warehouseId, long locationId) { LocationEnvelope response = client.get().uri(uri -> uri @@ -76,7 +90,7 @@ public class HttpPalletPhysicalGateway implements PalletPhysicalGateway { }).toBodilessEntity(); } - private record ResolveEnvelope(PalletReference data) { } + private record ResolveEnvelope(String code, String message, PalletReference data) { } private record LocationEnvelope(LocationReference data) { } private record TransitionEnvelope(Transition data) { } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java index 38ebc22c4..a536e8f10 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java @@ -367,6 +367,7 @@ public class PdaPalletOperationService { private PalletPhysicalGateway.PalletReference requirePallet(long warehouseId, String code) { PalletPhysicalGateway.PalletReference pallet = physical.resolve(code(code)); + if (pallet == null) throw notFound("未找到托盘"); if (pallet.warehouseId() != warehouseId) throw conflict("托盘不属于 Token 冻结的海外仓"); if ("SCRAPPED".equals(pallet.status())) throw conflict("已报废托盘不能执行作业"); return pallet; @@ -517,6 +518,7 @@ public class PdaPalletOperationService { private static long value(Long value) { return value == null ? 0L : value; } private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); } private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } + private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); } private record PlannedMove(long positionId, long packages, long expectedVersion, long sourcePalletId, String sourcePalletCode, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java index 01d2cf25a..85e274203 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java @@ -51,6 +51,7 @@ public class PdaInventoryMoveService { if (existing != null) return replay(existing, requestHash); var pallet = warehouse.resolvePallet(required(request.sourcePalletCode(), "Source pallet code is required")); + if (pallet == null) throw notFound("Source pallet was not found"); if (pallet.warehouseId() != warehouseId) throw conflict("Source pallet belongs to another overseas warehouse"); var target = warehouse.resolveLocation(warehouseId, required(request.targetLocationCode(), "Target location code is required")); @@ -188,5 +189,6 @@ public class PdaInventoryMoveService { private static long value(Long value) { return value == null ? 0 : value; } private static String required(String value, String message) { if (value == null || value.isBlank()) throw badRequest(message); return value.trim(); } private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } + private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); } private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java index b8500a76e..f7c70d02b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java @@ -42,6 +42,7 @@ public class PdaWarehouseService { public PdaWarehouseResponses.Pallet pallet(long warehouseId, PdaWarehouseRequests.Scan request) { WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.resolvePallet(code(request.code())); + if (pallet == null) throw notFound("Warehouse pallet was not found"); if (pallet.warehouseId() != warehouseId) throw conflict("Pallet belongs to another overseas warehouse"); WarehouseResourceGateway.WarehouseLocationReference location = warehouse.resolveLocation(warehouseId, pallet.locationId()); diff --git a/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml index c5b2c06a4..1fc1daacd 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml @@ -110,6 +110,7 @@ paths: requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMove' } } } } responses: '200': { description: 移库结果, content: { application/json: { schema: { $ref: '#/components/schemas/InventoryMoveResponse' } } } } + '404': { description: 来源托盘不存在(统一响应实际 HTTP 200,body.code=A0404), content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } } } '409': { description: 跨仓、资源或库存版本冲突 } /api/pda/v1/last-leg/inventory/resolve: post: @@ -130,6 +131,7 @@ paths: requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } } responses: '200': { description: 托盘及其库存内容, content: { application/json: { schema: { $ref: '#/components/schemas/PalletResponse' } } } } + '404': { description: 托盘不存在(统一响应实际 HTTP 200,body.code=A0404), content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } } } /api/pda/v1/last-leg/operations: get: summary: 查询当前操作人的仓内操作历史 @@ -143,7 +145,7 @@ paths: post: summary: 扫码查询托盘及库存内容 requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ScanCode' } } } } - responses: { '200': { description: Token绑定仓托盘内容 } } + responses: { '200': { description: Token绑定仓托盘内容 }, '404': { description: 托盘不存在(统一响应实际 HTTP 200,body.code=A0404), content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } } } } /api/pda/v1/last-leg/pallet-operations/assemble: post: summary: 将多个来源托盘的指定库存行组到目标托盘 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java index 4b26ac3ca..8d6acc50c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java @@ -6,8 +6,10 @@ import org.springframework.http.MediaType; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; +import org.springframework.web.server.ResponseStatusException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @@ -57,4 +59,17 @@ class HttpWarehouseResourceGatewayTest { server.verify(); } + @Test + void resolveMapsBusinessNotFoundEnvelopeToStableNotFound() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/MISSING")) + .andRespond(withSuccess(""" + {"code":"A0404","message":"Not found","data":null} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.resolvePallet("MISSING")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("404"); + server.verify(); + } + } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java new file mode 100644 index 000000000..a2ea91b5f --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java @@ -0,0 +1,40 @@ +package com.baiyue.lastleg.palletops.gateway; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; +import org.springframework.web.server.ResponseStatusException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +class HttpPalletPhysicalGatewayTest { + private HttpPalletPhysicalGateway gateway; + private MockRestServiceServer server; + + @BeforeEach + void setUp() { + RestClient.Builder builder = RestClient.builder(); + server = MockRestServiceServer.bindTo(builder).build(); + gateway = new HttpPalletPhysicalGateway(); + ReflectionTestUtils.setField(gateway, "client", builder.baseUrl("http://warehouse").build()); + ReflectionTestUtils.setField(gateway, "internalToken", "test-token"); + } + + @Test + void resolveMapsBusinessNotFoundEnvelopeToStableNotFound() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/MISSING")) + .andRespond(withSuccess(""" + {"code":"A0404","message":"Not found","data":null} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.resolve("MISSING")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("404"); + server.verify(); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java index ef90937a3..d88cb4b1a 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java @@ -63,6 +63,16 @@ class PdaPalletOperationServiceTest { verifyNoInteractions(mapper); } + @Test void resolveReportsMissingOnSitePalletAsNotFound() { + when(physical.resolve("MISSING")).thenReturn(null); + + ResponseStatusException failure = assertThrows(ResponseStatusException.class, () -> service.resolve(identity(), + new PalletOperationRequests.Scan("MISSING"))); + + assertEquals(404, failure.getStatusCode().value()); + verifyNoInteractions(mapper); + } + @Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() { var source = pallet(10L, "SOURCE", 88L, 1); var target = pallet(20L, "TARGET", 88L, 2); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java index 69c40e981..a4ea6b108 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java @@ -82,6 +82,7 @@ class PdaOperationsOpenApiContractTest { assertTrue(yaml.contains("enum: [LOCATION, PALLET]")); assertTrue(yaml.contains("enum: [APPROVE, REWORK]")); assertTrue(yaml.contains("enum: [DAMAGED, MISSING_LABEL, LABEL_MISMATCH, UNREADABLE, OTHER]")); + assertEquals(3, occurrences(yaml, "body.code=A0404")); assertFalse(yaml.contains("/api/pda/v1/last-leg/delivery")); for (String response : List.of("StocktakeSummaryPageResponse", "StocktakeDetailResponse", "StocktakeItemResponse", "InventoryMoveResponse", "InventoryScanResponse", "LocationResponse", @@ -101,4 +102,8 @@ class PdaOperationsOpenApiContractTest { return new String(input.readAllBytes(), StandardCharsets.UTF_8); } } + + private static int occurrences(String value, String token) { + return (value.length() - value.replace(token, "").length()) / token.length(); + } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java index 8ae98c938..00acb9c97 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java @@ -108,6 +108,16 @@ class PdaInventoryMoveServiceTest { verify(physical, never()).apply(anyLong(), any(), anyString(), anyLong(), anyString(), any()); } + @Test + void missingSourcePalletIsReportedAsNotFound() { + when(warehouse.resolvePallet("MISSING")).thenReturn(null); + + assertThatThrownBy(() -> service.move(900L, "operator-1", + new PdaInventoryMoveRequest("MISSING", "B-01", 2, "move-1", "库位优化"))) + .isInstanceOf(ResponseStatusException.class).hasMessageContaining("404"); + verify(physical, never()).apply(anyLong(), any(), anyString(), anyLong(), anyString(), any()); + } + @Test void resumesWhenWarehouseCommittedButLastlegTransactionRolledBack() { when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 21L, 3)); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseControllerTest.java new file mode 100644 index 000000000..6d29678bc --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseControllerTest.java @@ -0,0 +1,77 @@ +package com.baiyue.lastleg.pda.warehouse.controller; + +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.common.web.GlobalExceptionHandler; +import com.baiyue.lastleg.pda.warehouse.dto.PdaWarehouseRequests; +import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Set; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class PdaWarehouseControllerTest { + private PdaWarehouseService service; + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + service = mock(PdaWarehouseService.class); + PdaWarehouseController controller = new PdaWarehouseController(); + ReflectionTestUtils.setField(controller, "service", service); + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .setControllerAdvice(new GlobalExceptionHandler()) + .setCustomArgumentResolvers(new AuthenticatedUserResolver()) + .build(); + } + + @Test + void missingPalletReturnsHttp200WithStableA0404Body() throws Exception { + when(service.pallet(900L, new PdaWarehouseRequests.Scan("MISSING"))) + .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Warehouse pallet was not found")); + + mockMvc.perform(post("/api/pda/v1/last-leg/pallets/resolve") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"code\":\"MISSING\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("A0404")) + .andExpect(jsonPath("$.message").value("Warehouse pallet was not found")); + } + + private static AuthenticatedUser user() { + return new AuthenticatedUser(42L, 0L, 0L, 8L, "picker", "PLATFORM", "PDA", + Set.of("lastleg:pda:operate", "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), + false, null, null, false, 900L); + } + + private static final class AuthenticatedUserResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticationPrincipal.class) + && parameter.getParameterType() == AuthenticatedUser.class; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, + NativeWebRequest request, WebDataBinderFactory binderFactory) { + return user(); + } + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java index af623cbef..a487e38b5 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java @@ -48,6 +48,14 @@ class PdaWarehouseServiceTest { .isInstanceOf(ResponseStatusException.class).hasMessageContaining("409"); } + @Test + void missingPalletIsReportedAsNotFound() { + when(warehouse.resolvePallet("MISSING")).thenReturn(null); + + assertThatThrownBy(() -> service.pallet(900L, new PdaWarehouseRequests.Scan("MISSING"))) + .isInstanceOf(ResponseStatusException.class).hasMessageContaining("404"); + } + @Test void locationCombinesPhysicalCapacityAndInventoryOccupation() { when(warehouse.resolveLocation(900L, "A-01")).thenReturn(