From 990b74f3e8b5de1803ae8fb540915937cce7b2e7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 00:25:57 +0800 Subject: [PATCH] fix(lastleg): confirm pre-reserved pallets on PDA scan --- .../gateway/HttpWarehouseResourceGateway.java | 63 +++++++++---- .../pda/gateway/WarehouseResourceGateway.java | 4 + .../service/InboundPdaWorkflowService.java | 50 +++++++--- .../service/ReceivingContainerService.java | 66 ++++++++++--- .../HttpWarehouseResourceGatewayTest.java | 32 +++++++ .../InboundPdaWorkflowServiceTest.java | 92 +++++++++++++++++++ ...ReceivingContainerServiceBehaviorTest.java | 31 +++++++ 7 files changed, 295 insertions(+), 43 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 2b5043237..447b06d2b 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 @@ -117,23 +117,32 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { @Override public WarehousePalletReference resolvePallet(String palletCode) { - WarehousePalletResponse response = client.get() - .uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode) - .header("X-Internal-Service-Token", internalToken) - .retrieve().onStatus(HttpStatusCode::isError, (request, result) -> { - throw palletLookupFailure(result.getStatusCode()); - }).body(WarehousePalletResponse.class); - 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"); - } + WarehousePalletResponse response = lookupPallet(palletCode); 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()); + return reference(response.data()); + } + + @Override + public WarehousePalletReference resolveReservedPallet(String palletCode, String customerId, long warehouseId, + long palletId, long locationId, long resourceLockId) { + if (customerId == null || customerId.isBlank() || warehouseId <= 0 || palletId <= 0 + || locationId <= 0 || resourceLockId <= 0) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + "Reserved pallet binding is incomplete"); + } + try { + Long.parseLong(customerId); + } catch (NumberFormatException exception) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + "Reserved pallet customer binding is invalid"); + } + WarehousePalletData pallet = lookupPallet(palletCode).data(); + if (pallet.id() != palletId || pallet.warehouseId() != warehouseId || pallet.locationId() != locationId + || !"OCCUPIED".equals(pallet.status())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + "Scanned pallet does not match the reserved inbound plan resource"); + } + return reference(pallet); } @Override @@ -156,6 +165,28 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway { status.value() == 404 ? "Warehouse pallet was not found" : "Warehouse pallet lookup failed"); } + private WarehousePalletResponse lookupPallet(String palletCode) { + WarehousePalletResponse response = client.get() + .uri("/internal/v1/warehouse-pallets/resolve/{palletCode}", palletCode) + .header("X-Internal-Service-Token", internalToken) + .retrieve().onStatus(HttpStatusCode::isError, (request, result) -> { + throw palletLookupFailure(result.getStatusCode()); + }).body(WarehousePalletResponse.class); + 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; + } + + private WarehousePalletReference reference(WarehousePalletData pallet) { + return new WarehousePalletReference(pallet.id(), pallet.palletCode(), pallet.warehouseId(), + pallet.locationId(), pallet.status(), pallet.fbaWarehouseCode(), pallet.unitPrice(), pallet.version()); + } + private ResponseStatusException warehouseReservationFailure(HttpStatusCode status) { HttpStatus mapped = status.value() == 409 ? HttpStatus.CONFLICT : status.value() == 404 ? HttpStatus.NOT_FOUND diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java index ca8a41da2..9830668a5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java @@ -11,6 +11,10 @@ public interface WarehouseResourceGateway { void scrapPallet(String customerId, long palletId); WarehousePalletReference resolvePallet(String palletCode); WarehousePalletReference resolvePallet(long palletId); + default WarehousePalletReference resolveReservedPallet(String palletCode, String customerId, long warehouseId, + long palletId, long locationId, long resourceLockId) { + throw new UnsupportedOperationException("Reserved warehouse pallet resolution is not configured"); + } default WarehouseLocationReference resolveLocation(long warehouseId, String locationCode) { throw new UnsupportedOperationException("Warehouse location resolution is not configured"); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java index 78ca21628..578da55e5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java @@ -113,9 +113,8 @@ public class InboundPdaWorkflowService { InboundPlanPallet pallet = requirePallet(plan.getId(), planPalletId); validateDestinationWarehouse(pallet, request.warehouseId()); if (pallet.getWarehouseResourceLockId() != null) { - if (Objects.equals(pallet.getWarehousePalletId(), request.palletId()) - && Objects.equals(pallet.getWarehouseLocationId(), request.locationId())) return; - throw conflict("Planned pallet is already assigned to another warehouse resource"); + confirmReservedAssignment(plan, pallet, request.palletId(), request.locationId(), null); + return; } String destinationKey = destinationKey(pallet); String businessId = plan.getContainerNo() + ":" + destinationKey; @@ -130,12 +129,7 @@ public class InboundPdaWorkflowService { pallet.setAssignmentStatus("ASSIGNED"); pallet.setLockedAt(LocalDateTime.now()); repository.update(pallet); - List pallets = repository.pallets(plan.getId()); - if (!pallets.isEmpty() && pallets.stream().allMatch(value -> !isNotAssigned(value))) { - plan.setStatus("ASSIGNED"); - plan.setUpdatedAt(LocalDateTime.now()); - repository.update(plan); - } + promotePlanWhenAllPalletsAssigned(plan); } @Transactional @@ -167,13 +161,13 @@ public class InboundPdaWorkflowService { @Transactional public Long assignResolvedPallet(String customerId, InboundPlan plan, InboundPlanPallet pallet, WarehouseResourceGateway.WarehousePalletReference resource) { - if (pallet.getWarehouseResourceLockId() != null) { - if (Objects.equals(pallet.getWarehousePalletId(), resource.palletId())) return null; - throw conflict("Destination pallet is already bound to another physical pallet"); - } if (resource.palletNo() == null || resource.palletNo().isBlank()) { throw conflict("Resolved physical pallet has no pallet code"); } + if (pallet.getWarehouseResourceLockId() != null) { + confirmReservedAssignment(plan, pallet, resource.palletId(), resource.locationId(), resource.palletNo()); + return null; + } String destinationKey = destinationKey(pallet); String businessId = plan.getContainerNo() + ":" + destinationKey; String reservationKey = "INBOUND_CONTAINER_DESTINATION_PALLET:" + businessId + ":" + resource.palletId(); @@ -184,6 +178,7 @@ public class InboundPdaWorkflowService { pallet.setWarehouseLocationId(resource.locationId()); pallet.setWarehouseResourceLockId(lockId); pallet.setAssignmentStatus("ASSIGNED"); pallet.setLockedAt(LocalDateTime.now()); repository.update(pallet); + promotePlanWhenAllPalletsAssigned(plan); return lockId; } catch (RuntimeException failure) { compensateReservation(customerId, lockId, failure); @@ -878,6 +873,35 @@ public class InboundPdaWorkflowService { || !("ASSIGNED".equals(pallet.getAssignmentStatus()) || "LOCKED".equals(pallet.getAssignmentStatus())); } + private void confirmReservedAssignment(InboundPlan plan, InboundPlanPallet pallet, + long palletId, long locationId, String palletNo) { + boolean sameReservedResource = pallet.getWarehouseResourceLockId() != null + && pallet.getWarehouseResourceLockId() > 0 + && Objects.equals(pallet.getWarehousePalletId(), palletId) + && Objects.equals(pallet.getWarehouseLocationId(), locationId); + if (!sameReservedResource) { + throw conflict("Planned pallet is already assigned to another physical pallet or location"); + } + if ("ASSIGNED".equals(pallet.getAssignmentStatus()) || "LOCKED".equals(pallet.getAssignmentStatus())) return; + if (!"RESERVED".equals(pallet.getAssignmentStatus())) { + throw conflict("Reserved physical pallet has an invalid assignment status"); + } + if (palletNo != null && !palletNo.isBlank()) pallet.setPalletNo(palletNo.trim()); + pallet.setAssignmentStatus("ASSIGNED"); + if (pallet.getLockedAt() == null) pallet.setLockedAt(LocalDateTime.now()); + repository.update(pallet); + promotePlanWhenAllPalletsAssigned(plan); + } + + private void promotePlanWhenAllPalletsAssigned(InboundPlan plan) { + List plannedPallets = repository.pallets(plan.getId()); + if (plannedPallets.isEmpty() || plannedPallets.stream().anyMatch(this::isNotAssigned)) return; + if (!java.util.Set.of("PENDING_ASSIGNMENT", "RESERVED").contains(plan.getStatus())) return; + plan.setStatus("ASSIGNED"); + plan.setUpdatedAt(LocalDateTime.now()); + repository.update(plan); + } + private InboundPlan requirePlan(String customerId, String planNo) { InboundPlan plan = repository.lockPlan(customerId, planNo); if (plan == null) throw notFound("Inbound plan not found"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java index 3dd5353c4..67b4082c7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java @@ -274,14 +274,8 @@ public class ReceivingContainerService { ReceivingContainerSession session = requireSigned(operatorKey); String customerId = session.getCustomerId(); InboundRef ref = requireInbound(customerId, session.getContainerNo(), request.inboundCode()); - WarehouseResourceGateway.WarehousePalletReference pallet; - try { - pallet = warehouse.resolvePallet(request.palletCode()); - } catch (IllegalArgumentException exception) { - throw notFound("Pallet was not found"); - } - if (pallet == null) throw notFound("Pallet was not found"); - validatePallet(ref, pallet); + PalletSelection selection = resolveReceivingPallet(ref, request.palletCode(), false); + WarehouseResourceGateway.WarehousePalletReference pallet = selection.resource(); return new ReceivingContainerResponses.PalletResolved(pallet.palletNo(), pallet.warehouseId(), pallet.status(), pallet.fbaWarehouseCode(), ref.line().getInboundCode(), destinationType(ref.line(), ref.order()), true); } @@ -301,9 +295,9 @@ public class ReceivingContainerService { List skuAllocations = allocateFbaSkuReceipts(customerId, fbaPlanRefs, request.packageCount()); skuAllocations.forEach(allocation -> workflow.validateBulkResolved(customerId, allocation.ref().plan(), allocation.ref().line(), allocation.packages())); - WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode()); - validatePallet(ref, resource); - InboundPlanPallet pallet = choosePallet(ref, resource); + PalletSelection selection = resolveReceivingPallet(ref, request.palletCode(), true); + WarehouseResourceGateway.WarehousePalletReference resource = selection.resource(); + InboundPlanPallet pallet = selection.pallet(); Long resourceLockId = workflow.assignResolvedPallet(customerId, ref.plan(), pallet, resource); try { int accepted = 0; @@ -344,9 +338,9 @@ public class ReceivingContainerService { if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) { throw conflict("FBA destination must use bulk count"); } - WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode()); - validatePallet(ref, resource); - InboundPlanPallet pallet = choosePallet(ref, resource); + PalletSelection selection = resolveReceivingPallet(ref, request.palletCode(), true); + WarehouseResourceGateway.WarehousePalletReference resource = selection.resource(); + InboundPlanPallet pallet = selection.pallet(); workflow.validateCartonResolved(customerId, ref.plan(), ref.line(), pallet, request.cartonMark()); Long resourceLockId = workflow.assignResolvedPallet(customerId, ref.plan(), pallet, resource); try { @@ -640,6 +634,47 @@ public class ReceivingContainerService { } private void validatePallet(InboundRef ref, WarehouseResourceGateway.WarehousePalletReference pallet) { + private PalletSelection resolveReceivingPallet(InboundRef ref, String palletCode, boolean requirePlanPallet) { + List reserved = pallets.selectList(new LambdaQueryWrapper() + .eq(InboundPlanPallet::getCustomerId, ref.order().getCustomerId()) + .eq(InboundPlanPallet::getPlanId, ref.plan().getId())).stream() + .filter(value -> sameDestination(ref.line(), value)) + .filter(value -> palletCode.trim().equals(value.getPalletNo())) + .filter(this::hasConfirmedReservation) + .toList(); + if (reserved.size() > 1) { + throw conflict("Pallet code matches multiple reserved resources in this inbound plan"); + } + WarehouseResourceGateway.WarehousePalletReference resource; + InboundPlanPallet pallet; + if (reserved.size() == 1) { + pallet = reserved.get(0); + long warehouseId = ref.plan().getOverseasWarehouseId() == null + ? Long.parseLong(ref.order().getOverseasWarehouseId()) : ref.plan().getOverseasWarehouseId(); + resource = warehouse.resolveReservedPallet(palletCode.trim(), ref.order().getCustomerId(), warehouseId, + pallet.getWarehousePalletId(), pallet.getWarehouseLocationId(), pallet.getWarehouseResourceLockId()); + } else { + try { + resource = warehouse.resolvePallet(palletCode.trim()); + } catch (IllegalArgumentException exception) { + throw notFound("Pallet was not found"); + } + if (resource == null) throw notFound("Pallet was not found"); + pallet = requirePlanPallet ? choosePallet(ref, resource) : null; + } + validatePallet(ref, resource); + return new PalletSelection(pallet, resource); + } + + private boolean hasConfirmedReservation(InboundPlanPallet pallet) { + return pallet.getWarehousePalletId() != null && pallet.getWarehousePalletId() > 0 + && pallet.getWarehouseLocationId() != null && pallet.getWarehouseLocationId() > 0 + && pallet.getWarehouseResourceLockId() != null && pallet.getWarehouseResourceLockId() > 0 + && ("RESERVED".equals(pallet.getAssignmentStatus()) + || "ASSIGNED".equals(pallet.getAssignmentStatus()) + || "LOCKED".equals(pallet.getAssignmentStatus())); + } + if ("SCRAPPED".equals(pallet.status())) throw conflict("Pallet is scrapped"); long actualWarehouse; try { actualWarehouse = Long.parseLong(ref.order().getOverseasWarehouseId()); } @@ -667,6 +702,9 @@ public class ReceivingContainerService { } private ReceivingContainerResponses.Session snapshot(ReceivingContainerSession session, List containerPlans) { + private record PalletSelection(InboundPlanPallet pallet, + WarehouseResourceGateway.WarehousePalletReference resource) { } + List inbounds = refs(session.getCustomerId(), session.getContainerNo()).stream() .map(this::inbound).toList(); int total = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum(); 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 1da4230b5..64a1fb152 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 @@ -66,6 +66,38 @@ class HttpWarehouseResourceGatewayTest { server.verify(); } + @Test + void resolveReservedAcceptsOnlyExactOccupiedPlanBinding() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-RESERVED")) + .andRespond(withSuccess(""" + {"code":"200","data":{"palletId":31,"palletCode":"PAL-RESERVED", + "warehouseId":21,"locationId":41,"status":"OCCUPIED","version":3, + "reservable":false,"reservationBlockReason":"PALLET_NOT_IDLE"}} + """, MediaType.APPLICATION_JSON)); + + var result = gateway.resolveReservedPallet("PAL-RESERVED", "11", 21L, 31L, 41L, 51L); + + assertThat(result.palletId()).isEqualTo(31L); + assertThat(result.status()).isEqualTo("OCCUPIED"); + server.verify(); + } + + @Test + void resolveReservedRejectsDifferentPhysicalBinding() { + server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-RESERVED")) + .andRespond(withSuccess(""" + {"code":"200","data":{"palletId":31,"palletCode":"PAL-RESERVED", + "warehouseId":21,"locationId":99,"status":"OCCUPIED","version":3, + "reservable":false,"reservationBlockReason":"PALLET_NOT_IDLE"}} + """, MediaType.APPLICATION_JSON)); + + assertThatThrownBy(() -> gateway.resolveReservedPallet("PAL-RESERVED", "11", 21L, 31L, 41L, 51L)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("409") + .hasMessageContaining("does not match"); + server.verify(); + } + @Test void resolveFailsClosedWhenWarehouseOmitsReservationFacts() { server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN")) diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java index f8093b7c4..f91971b8a 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java @@ -188,6 +188,98 @@ class InboundPdaWorkflowServiceTest { verify(repository).update(targetPallet); } + @Test + void reservedPalletScanConfirmsOnSiteWithoutReservingAgainAndAdvancesPlan() { + plan.setStatus("RESERVED"); + targetPallet.setPalletNo("IP-ACY2-20260909-P001"); + targetPallet.setWarehousePalletId(188L); + targetPallet.setWarehouseLocationId(288L); + targetPallet.setWarehouseResourceLockId(999L); + targetPallet.setAssignmentStatus("RESERVED"); + targetPallet.setLockedAt(null); + WarehouseResourceGateway.WarehousePalletReference resource = + new WarehouseResourceGateway.WarehousePalletReference(188L, "A002", 88L, 288L, + "OCCUPIED", "ACY2", 1); + when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet)); + + service.assignResolvedPallet("100", plan, targetPallet, resource); + service.assignResolvedPallet("100", plan, targetPallet, resource); + + assertEquals("A002", targetPallet.getPalletNo()); + assertEquals("ASSIGNED", targetPallet.getAssignmentStatus()); + org.junit.jupiter.api.Assertions.assertNotNull(targetPallet.getLockedAt()); + assertEquals("ASSIGNED", plan.getStatus()); + verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(), any(), any(), any()); + verify(repository, times(1)).update(targetPallet); + verify(repository, times(1)).update(plan); + } + + @Test + void publicAssignmentAlsoConfirmsReservedPalletWithoutReservingAgain() { + plan.setStatus("PENDING_ASSIGNMENT"); + targetPallet.setDestinationWarehouseId("88"); + targetPallet.setWarehousePalletId(188L); + targetPallet.setWarehouseLocationId(288L); + targetPallet.setWarehouseResourceLockId(999L); + targetPallet.setAssignmentStatus("RESERVED"); + targetPallet.setLockedAt(null); + when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet); + when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet)); + + service.assign("100", "LIP1", targetPallet.getId(), + new PdaRequests.Assign(88L, 188L, 288L, "reserved-public")); + service.assign("100", "LIP1", targetPallet.getId(), + new PdaRequests.Assign(88L, 188L, 288L, "reserved-public-retry")); + + assertEquals("ASSIGNED", targetPallet.getAssignmentStatus()); + assertEquals("ASSIGNED", plan.getStatus()); + verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(), any(), any(), any()); + verify(repository, times(1)).update(targetPallet); + verify(repository, times(1)).update(plan); + } + + @Test + void reservedConfirmationDoesNotRegressAdvancedPlanStatus() { + plan.setStatus("SIGNED"); + targetPallet.setWarehousePalletId(188L); + targetPallet.setWarehouseLocationId(288L); + targetPallet.setWarehouseResourceLockId(999L); + targetPallet.setAssignmentStatus("RESERVED"); + WarehouseResourceGateway.WarehousePalletReference resource = + new WarehouseResourceGateway.WarehousePalletReference(188L, "A002", 88L, 288L, + "OCCUPIED", "ACY2", 1); + when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet)); + + service.assignResolvedPallet("100", plan, targetPallet, resource); + + assertEquals("ASSIGNED", targetPallet.getAssignmentStatus()); + assertEquals("SIGNED", plan.getStatus()); + verify(repository, never()).update(plan); + } + + @Test + void reservedPalletScanRejectsAnotherPhysicalPalletOrLocation() { + targetPallet.setWarehousePalletId(188L); + targetPallet.setWarehouseLocationId(288L); + targetPallet.setWarehouseResourceLockId(999L); + targetPallet.setAssignmentStatus("RESERVED"); + WarehouseResourceGateway.WarehousePalletReference anotherPallet = + new WarehouseResourceGateway.WarehousePalletReference(189L, "A003", 88L, 288L, + "IDLE", "ACY2", 1); + WarehouseResourceGateway.WarehousePalletReference anotherLocation = + new WarehouseResourceGateway.WarehousePalletReference(188L, "A002", 88L, 289L, + "IDLE", "ACY2", 1); + + assertThrows(ResponseStatusException.class, + () -> service.assignResolvedPallet("100", plan, targetPallet, anotherPallet)); + assertThrows(ResponseStatusException.class, + () -> service.assignResolvedPallet("100", plan, targetPallet, anotherLocation)); + + assertEquals("RESERVED", targetPallet.getAssignmentStatus()); + verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(), any(), any(), any()); + verify(repository, never()).update(targetPallet); + } + @Test void resolvedAssignmentRejectsPhysicalPalletWithoutBusinessCodeBeforeReservation() { targetPallet.setWarehouseResourceLockId(null); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java index 0a72f267c..3b9fbcc2b 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java @@ -465,6 +465,37 @@ class ReceivingContainerServiceBehaviorTest { verify(workflow).compensateReservation("100", 55L, failure); } + @Test + void bulkReceivingResolvesExactPreReservedPalletWithoutIdleLookup() { + stubCurrent("FBA", "CONT-A"); + stubSku(); + InboundPlanPallet reserved = planPallet(); + reserved.setPalletNo("PAL-RESERVED"); + reserved.setWarehousePalletId(30L); + reserved.setWarehouseLocationId(40L); + reserved.setWarehouseResourceLockId(50L); + reserved.setAssignmentStatus("RESERVED"); + when(pallets.selectList(any())).thenReturn(List.of(reserved)); + WarehouseResourceGateway.WarehousePalletReference occupied = + new WarehouseResourceGateway.WarehousePalletReference(30L, "PAL-RESERVED", 100L, 40L, + "OCCUPIED", "FBA-1", 0); + when(warehouse.resolveReservedPallet("PAL-RESERVED", "100", 100L, 30L, 40L, 50L)) + .thenReturn(occupied); + when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any())) + .thenReturn(1); + when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1); + when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1); + + service.receiveBulk("7", "receiver", + new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-RESERVED", 1, "reserved-key")); + + verify(warehouse).resolveReservedPallet("PAL-RESERVED", "100", 100L, 30L, 40L, 50L); + verify(warehouse, never()).resolvePallet("PAL-RESERVED"); + verify(workflow).assignResolvedPallet(org.mockito.ArgumentMatchers.eq("100"), + any(InboundPlan.class), org.mockito.ArgumentMatchers.same(reserved), + org.mockito.ArgumentMatchers.same(occupied)); + } + @Test void signRequiresEachTypedPhotoExactlyOnceAndRestoresRoles() { when(sessions.lockActiveByOperator("OPERATOR:7")).thenReturn(session("CONT-A", "WAIT_SIGN"));