fix(lastleg): confirm pre-reserved pallets on PDA scan

This commit is contained in:
2026-09-09 00:25:57 +08:00
parent 7b47de47c6
commit 990b74f3e8
7 changed files with 295 additions and 43 deletions
@@ -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"))
@@ -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);
@@ -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"));