fix(firstleg): preserve sealed pallet identity in atomic PDA picking
This commit is contained in:
+5
@@ -65,7 +65,12 @@ class PdaConvergenceOpenApiContractTest {
|
||||
.contains("loadingPlanId:", "containerBookingId:")
|
||||
.doesNotContain("containerNo:");
|
||||
assertThat(batchItemRequest)
|
||||
.contains("PICK_PALLET", "palletNo:", "every pickItem", "No carton quantities")
|
||||
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
|
||||
String pickItem = yaml.substring(yaml.indexOf(" PickItem:"), yaml.indexOf(" PendingTask:"));
|
||||
assertThat(pickItem).contains("unitType:", "palletId:", "palletNo:", "indivisible:", "SEALED_PALLET");
|
||||
String inventoryUnits = yaml.substring(yaml.indexOf(" InventorySnapshot:"), yaml.indexOf(" InboundInventory:"));
|
||||
assertThat(inventoryUnits).contains("units:", "InventoryUnit:", "pickItemId:", "loadingPlanItemId:", "SEALED_PALLET", "indivisible:");
|
||||
assertThat(inventoryResource)
|
||||
.contains("blockId:", "slotNo:", "reservationId:", "weightResultId:", "slotStatus:")
|
||||
.doesNotContain("occupancyId:");
|
||||
|
||||
+14
@@ -10,6 +10,20 @@ import java.lang.reflect.Modifier;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LoadingPickPdaTaskSqlContractTest {
|
||||
@Test
|
||||
void pickingUnitSnapshotsComeFromTheSameAuthoritativeLoadingPlanInEveryReadPath() throws Exception {
|
||||
for (var method : LoadingPickItemMapper.class.getDeclaredMethods()) {
|
||||
if (!java.util.Set.of("selectTaskItemsWithPlan", "selectTaskItemsWithAccount", "selectTaskItemWithAccount")
|
||||
.contains(method.getName())) continue;
|
||||
String sql = method.getAnnotation(Select.class).value()[0];
|
||||
assertThat(sql).contains("plan_item.unit_type", "plan_item.pallet_id", "plan_item.pallet_no", "plan_item.indivisible")
|
||||
.contains("plan_item.id = pick_item.loading_plan_item_id");
|
||||
assertThat(sql).contains(method.getName().equals("selectTaskItemsWithPlan")
|
||||
? "plan_item.loading_plan_id = task.loading_plan_id" : "plan.id = pick_task.loading_plan_id");
|
||||
}
|
||||
assertThat(LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithPlan", long.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryRowsAreConcreteBeansThatMyBatisCanInstantiate() throws Exception {
|
||||
assertThat(LoadingPickTaskMapper.PendingTaskRow.class.isInterface()).isFalse();
|
||||
|
||||
@@ -462,8 +462,13 @@ class LoadingPickServiceTest {
|
||||
@Test
|
||||
void taskDetailReturnsServerInboundAndLoadingVersions() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity sealedPallet = item();
|
||||
sealedPallet.setUnitType("SEALED_PALLET");
|
||||
sealedPallet.setPalletId(801L);
|
||||
sealedPallet.setPalletNo("DP-260909-0001");
|
||||
sealedPallet.setIndivisible(true);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(sealedPallet));
|
||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||
when(repository.assignment(20L)).thenReturn(null);
|
||||
when(repository.review(20L)).thenReturn(null);
|
||||
@@ -483,6 +488,8 @@ class LoadingPickServiceTest {
|
||||
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
||||
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
||||
assertThat(result.mergedIntoContainerBookingId()).isNull();
|
||||
assertThat(result.items().get(0)).extracting("unitType", "palletId", "palletNo", "indivisible")
|
||||
.containsExactly("SEALED_PALLET", 801L, "DP-260909-0001", true);
|
||||
assertThat(result.items().get(0).inventoryResources()).singleElement().satisfies(resource -> {
|
||||
assertThat(resource.blockId()).isEqualTo(71L);
|
||||
assertThat(resource.slotNo()).isEqualTo(1);
|
||||
@@ -1200,14 +1207,27 @@ class LoadingPickServiceTest {
|
||||
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
List.of(allocation(invocation.getArgument(1), 2)));
|
||||
var snapshot = service.inventorySnapshot(20L, 101L);
|
||||
assertThat(snapshot.units()).hasSize(2);
|
||||
picked.setUnitType("SEALED_PALLET");
|
||||
picked.setPalletId(801L);
|
||||
picked.setPalletNo("DP-260909-0001");
|
||||
picked.setIndivisible(true);
|
||||
var palletSnapshot = service.inventorySnapshot(20L, 101L);
|
||||
assertThat(palletSnapshot.units().get(0)).extracting("pickItemId", "unitType", "palletId", "palletNo", "indivisible")
|
||||
.containsExactly(30L, "SEALED_PALLET", 801L, "DP-260909-0001", true);
|
||||
assertThat(palletSnapshot.snapshotToken()).isNotEqualTo(snapshot.snapshotToken());
|
||||
picked.setUnitType(null);
|
||||
picked.setPalletId(null);
|
||||
picked.setPalletNo(null);
|
||||
picked.setIndivisible(null);
|
||||
String snapshotToken = snapshot.snapshotToken();
|
||||
assertThat(snapshot.inbounds()).hasSize(2);
|
||||
assertThat(snapshot.inbounds().get(0).resources().get(0))
|
||||
.extracting("blockId", "slotNo", "reservationId", "locationId", "availablePackageCount")
|
||||
.containsExactly(71L, 1, 72L, 74L, 2);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
||||
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null, null),
|
||||
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton", null)));
|
||||
|
||||
service.batch(20L, "batch-1", request, 9L, "operator");
|
||||
service.batch(20L, "batch-1", request, 9L, "operator");
|
||||
@@ -1248,7 +1268,7 @@ class LoadingPickServiceTest {
|
||||
List.of(allocation(invocation.getArgument(1), 100)));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||
new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 100, null, 0, null, null)));
|
||||
|
||||
service.batch(20L, "batch-reset-plan", request, 9L, "operator");
|
||||
|
||||
@@ -1274,8 +1294,8 @@ class LoadingPickServiceTest {
|
||||
when(repository.items(20L)).thenReturn(items);
|
||||
when(repository.items(20L, 101L)).thenReturn(items);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
||||
new BatchItemRequest(31L, "PARTIAL", null, 4, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null, null),
|
||||
new BatchItemRequest(31L, "PARTIAL", null, 4, 0, null, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-invalid", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class);
|
||||
@@ -1286,6 +1306,87 @@ class LoadingPickServiceTest {
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
}
|
||||
|
||||
private List<LoadingPickItemEntity> prepareWholePalletBatch() {
|
||||
var first = item();
|
||||
var second = item();
|
||||
second.setId(31L);
|
||||
second.setLoadingPlanItemId(41L);
|
||||
second.setInboundDetailId(61L);
|
||||
var items = List.of(first, second);
|
||||
for (var item : items) {
|
||||
item.setUnitType("SEALED_PALLET"); item.setPalletId(801L); item.setPalletNo("DP-260909-0001"); item.setIndivisible(true);
|
||||
}
|
||||
when(repository.task(20L)).thenReturn(task());
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task());
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(items);
|
||||
when(repository.items(20L, 101L)).thenReturn(items);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(1L, 50L)));
|
||||
when(palletAllocations.list(1L, 50L)).thenReturn(List.of(allocation(50L, 4)));
|
||||
when(dropshipPallets.sealedPalletLines(801L)).thenReturn(items.stream().map(item -> {
|
||||
var line = new DropshipPalletizationMapper.PalletLine();
|
||||
line.setPalletId(801L); line.setPalletNo("DP-260909-0001"); line.setPalletStatus("SEALED");
|
||||
line.setWarehouseId(101L); line.setInboundId(50L); line.setInboundDetailId(item.getInboundDetailId()); line.setCartonCount(2);
|
||||
return line;
|
||||
}).toList());
|
||||
return items;
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchPicksEveryLineOfSealedPalletOnceAndReplaysWithoutSecondWrite() {
|
||||
prepareWholePalletBatch();
|
||||
AtomicReference<LoadingPickOperationEntity> operation = new AtomicReference<>();
|
||||
when(repository.operation("batch-pallet")).thenAnswer(invocation -> operation.get());
|
||||
doAnswer(invocation -> { operation.set(invocation.getArgument(0)); return null; })
|
||||
.when(repository).insert(any(LoadingPickOperationEntity.class));
|
||||
when(repository.pick(anyLong(), eq(2), eq(0), eq(101L))).thenReturn(1);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
var snapshot = service.inventorySnapshot(20L, 101L);
|
||||
assertThat(snapshot.units()).hasSize(2).allSatisfy(unit -> assertThat(unit.palletNo()).isEqualTo("DP-260909-0001"));
|
||||
var request = new BatchRequest(101L, 10L, 99L, 0, snapshot.snapshotToken(), List.of(
|
||||
new BatchItemRequest(30L, "PICK_PALLET", null, null, 0, null, "DP-260909-0001"),
|
||||
new BatchItemRequest(31L, "PICK_PALLET", null, null, 0, null, "DP-260909-0001")));
|
||||
service.batch(20L, "batch-pallet", request, 9L, "operator");
|
||||
service.batch(20L, "batch-pallet", request, 9L, "operator");
|
||||
verify(repository, times(1)).pick(30L, 2, 0, 101L);
|
||||
verify(repository, times(1)).pick(31L, 2, 0, 101L);
|
||||
verify(repository, times(1)).touchTask(20L, 0);
|
||||
verify(repository, times(1)).syncInboundProjections(20L, 9L, "operator");
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-pallet", new BatchRequest(101L, 10L, 99L, 0,
|
||||
snapshot.snapshotToken(), List.of(new BatchItemRequest(30L, "PICK_PALLET", null, null, 0, null, "DP-260909-0999"))), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"missing-row", "ordinary", "partial", "skip", "quantity", "wrong-code", "unsealed", "wrong-warehouse", "physical-short", "stale-item", "stale-task", "stale-snapshot"})
|
||||
void rejectsInvalidWholePalletBatchBeforeAnyWrite(String scenario) {
|
||||
var items = prepareWholePalletBatch();
|
||||
String snapshot = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
var lines = new java.util.ArrayList<>(List.of(
|
||||
new BatchItemRequest(30L, "PICK_PALLET", null, null, 0, null, "DP-260909-0001"),
|
||||
new BatchItemRequest(31L, "PICK_PALLET", null, null, 0, null, "DP-260909-0001")));
|
||||
switch (scenario) {
|
||||
case "missing-row" -> lines.remove(1);
|
||||
case "ordinary" -> lines.set(1, new BatchItemRequest(31L, "PICK", 2, null, 0, null, null));
|
||||
case "partial" -> lines.set(1, new BatchItemRequest(31L, "PARTIAL", null, 1, 0, null, null));
|
||||
case "skip" -> lines.set(1, new BatchItemRequest(31L, "SKIP", null, null, 0, "skip", null));
|
||||
case "quantity" -> lines.set(1, new BatchItemRequest(31L, "PICK_PALLET", 2, null, 0, null, "DP-260909-0001"));
|
||||
case "wrong-code" -> lines.set(1, new BatchItemRequest(31L, "PICK_PALLET", null, null, 0, null, "DP-260909-0999"));
|
||||
case "unsealed" -> dropshipPallets.sealedPalletLines(801L).get(0).setPalletStatus("OPEN");
|
||||
case "wrong-warehouse" -> dropshipPallets.sealedPalletLines(801L).get(0).setWarehouseId(999L);
|
||||
case "physical-short" -> dropshipPallets.sealedPalletLines(801L).get(0).setCartonCount(1);
|
||||
case "stale-item" -> items.get(1).setVersion(9);
|
||||
case "stale-snapshot" -> snapshot = "0".repeat(64);
|
||||
}
|
||||
var request = new BatchRequest(101L, 10L, 99L, "stale-task".equals(scenario) ? 9 : 0, snapshot, lines);
|
||||
assertThatThrownBy(() -> service.batch(20L, "invalid-pallet", request, 9L, "operator")).isInstanceOf(BizException.class);
|
||||
verify(repository, never()).pick(anyLong(), anyInt(), anyInt(), anyLong());
|
||||
verify(repository, never()).partial(anyLong(), anyInt(), anyInt(), anyLong());
|
||||
verify(repository, never()).skip(anyLong(), anyInt(), anyLong());
|
||||
verify(repository, never()).touchTask(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsChangedInventorySnapshotBeforeWritingAnyBatchLine() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -1299,7 +1400,7 @@ class LoadingPickServiceTest {
|
||||
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
List.of(allocation(invocation.getArgument(1), 2)));
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-inventory-stale", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -1317,7 +1418,7 @@ class LoadingPickServiceTest {
|
||||
task.setVersion(3);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 2, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", request, 9L, "operator"))
|
||||
.isInstanceOf(BizException.class);
|
||||
@@ -1343,7 +1444,7 @@ class LoadingPickServiceTest {
|
||||
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
List.of(allocation(invocation.getArgument(1), 2)));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
|
||||
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null, null);
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest(
|
||||
101L, 10L, 99L, 1, snapshotToken, List.of(line)), 9L, "operator"))
|
||||
@@ -1376,7 +1477,7 @@ class LoadingPickServiceTest {
|
||||
|
||||
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, 1,
|
||||
"stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null, null)));
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest,
|
||||
9L, "operator")).isInstanceOf(BizException.class);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user