From 6908969307169e4863007ce38da0a0ab79971cbf Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 8 Sep 2026 14:52:08 +0800 Subject: [PATCH] fix(lastleg): reject unavailable receiving pallets --- .../gateway/HttpWarehouseResourceGateway.java | 42 +++++++++-- .../HttpWarehouseResourceGatewayTest.java | 71 ++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) 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 db2b47740..a5683449d 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 @@ -59,10 +59,20 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { "businessId", businessId, "reservationKey", reservationKey)) .retrieve() .onStatus(HttpStatusCode::isError, (request, result) -> { - throw new IllegalStateException("Warehouse rejected pallet reservation: HTTP " + result.getStatusCode()); + throw warehouseReservationFailure(result.getStatusCode()); }) .body(ReserveEnvelope.class); - if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned an empty lock response"); + if (response == null) { + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, + "Warehouse resource reservation returned no response"); + } + if (response.data() == null) { + HttpStatus status = "A0409".equals(response.code()) ? HttpStatus.CONFLICT + : "A0404".equals(response.code()) ? HttpStatus.NOT_FOUND : HttpStatus.SERVICE_UNAVAILABLE; + throw new ResponseStatusException(status, + response.message() == null || response.message().isBlank() + ? "Warehouse resource reservation was rejected" : response.message()); + } return response.data().lockId(); } @@ -120,6 +130,7 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Warehouse pallet service returned no usable response"); } + requireReservable(response.data()); return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().warehouseId(), response.data().locationId(), response.data().status(), response.data().fbaWarehouseCode(), response.data().unitPrice(), response.data().version()); @@ -133,6 +144,7 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { .body(WarehousePalletResponse.class); if (response == null || response.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Warehouse pallet service returned no usable response"); + requireReservable(response.data()); return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().warehouseId(), response.data().locationId(), response.data().status(), response.data().fbaWarehouseCode(), response.data().unitPrice(), response.data().version()); @@ -144,6 +156,26 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { status.value() == 404 ? "Warehouse pallet was not found" : "Warehouse pallet lookup failed"); } + private ResponseStatusException warehouseReservationFailure(HttpStatusCode status) { + HttpStatus mapped = status.value() == 409 ? HttpStatus.CONFLICT + : status.value() == 404 ? HttpStatus.NOT_FOUND + : status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE; + return new ResponseStatusException(mapped, "Warehouse resource reservation was rejected"); + } + + private void requireReservable(WarehousePalletData pallet) { + if (pallet.reservable()) return; + String reason = switch (pallet.reservationBlockReason() == null + ? "RESOURCE_STATE_UNKNOWN" : pallet.reservationBlockReason()) { + case "PALLET_NOT_IDLE" -> "Pallet is not idle"; + case "LOCATION_NOT_FOUND" -> "Pallet location was not found"; + case "VIRTUAL_LOCATION_NOT_ACTIVE" -> "Pallet virtual location is not active"; + case "LOCATION_NOT_IDLE" -> "Pallet location is not idle"; + default -> "Pallet resource availability could not be verified"; + }; + throw new ResponseStatusException(HttpStatus.CONFLICT, reason); + } + @Override public WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) { WarehouseLocationResponse response = client.get() @@ -200,8 +232,10 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { 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, java.math.BigDecimal unitPrice, int version) {} - private record ReserveEnvelope(ReserveData data) {} + String status, String fbaWarehouseCode, java.math.BigDecimal unitPrice, int version, + String locationType, String locationStatus, boolean reservable, + String reservationBlockReason) {} + private record ReserveEnvelope(String code, String message, ReserveData data) {} private record ReserveData(long lockId) {} private record WarehouseReferenceResponse(String code, String message, WarehouseReference data) {} private record WarehouseLocationResponse(String code, String message, WarehouseLocationReference data) {} 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 8d6acc50c..be03e6fa4 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 @@ -11,6 +11,7 @@ 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.withStatus; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; class HttpWarehouseResourceGatewayTest { @@ -32,7 +33,9 @@ class HttpWarehouseResourceGatewayTest { .andRespond(withSuccess(""" {"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-1", "warehouseId":2092040869757231105,"locationId":2092040891076878337, - "status":"IDLE","fbaWarehouseCode":"ABE3","version":3}} + "status":"IDLE","fbaWarehouseCode":"ABE3","version":3, + "locationType":"STORAGE","locationStatus":"IDLE","reservable":true, + "reservationBlockReason":null}} """, MediaType.APPLICATION_JSON)); WarehouseResourceGateway.WarehousePalletReference result = gateway.resolvePallet("PAL-1"); @@ -43,6 +46,40 @@ class HttpWarehouseResourceGatewayTest { server.verify(); } + @Test + void resolveRejectsPalletWhoseLocationIsNotReservable() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-BLOCKED")) + .andRespond(withSuccess(""" + {"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-BLOCKED", + "warehouseId":2092040869757231105,"locationId":2092040891076878337, + "status":"IDLE","fbaWarehouseCode":"ABE3","version":3, + "locationType":"STORAGE","locationStatus":"OCCUPIED","reservable":false, + "reservationBlockReason":"LOCATION_NOT_IDLE"}} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.resolvePallet("PAL-BLOCKED")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("409") + .hasMessageContaining("Pallet location is not idle"); + server.verify(); + } + + @Test + void resolveFailsClosedWhenWarehouseOmitsReservationFacts() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN")) + .andRespond(withSuccess(""" + {"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-UNKNOWN", + "warehouseId":2092040869757231105,"locationId":2092040891076878337, + "status":"IDLE","fbaWarehouseCode":"ABE3","version":3}} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.resolvePallet("PAL-UNKNOWN")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("409") + .hasMessageContaining("availability could not be verified"); + server.verify(); + } + @Test void ensureDropshipStillMapsWarehouseEntityId() { server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/ensure-dropship")) @@ -72,4 +109,36 @@ class HttpWarehouseResourceGatewayTest { server.verify(); } + @Test + void reserveMapsWarehouseLocationConflictToStableConflict() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks")) + .andRespond(withStatus(org.springframework.http.HttpStatus.CONFLICT) + .contentType(MediaType.APPLICATION_JSON) + .body(""" + {"code":"A0409","message":"Location is not idle","data":null} + """)); + + assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L, + "LASTLEG_INBOUND", "plan:5", "reservation-1")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("409") + .hasMessageContaining("Warehouse resource reservation was rejected"); + server.verify(); + } + + @Test + void reserveMapsBusinessConflictEnvelopeToStableConflict() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks")) + .andRespond(withSuccess(""" + {"code":"A0409","message":"Location is not idle","data":null} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L, + "LASTLEG_INBOUND", "plan:5", "reservation-1")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("409") + .hasMessageContaining("Location is not idle"); + server.verify(); + } + }