This commit is contained in:
2026-09-01 11:09:14 +08:00
191 changed files with 6486 additions and 403 deletions
@@ -12,6 +12,8 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -51,6 +53,7 @@ class InboundPdaWorkflowServiceTest {
@Mock InboundUnloadCompletedOutboxService unloadCompletedOutbox;
@Mock LastlegOrderMapper orders;
@Mock UnloadPhotoFileGateway unloadPhotoFiles;
@Mock DropshipOrderService dropshipOrders;
private InboundPdaWorkflowService service;
private InboundPlan plan;
@@ -73,6 +76,7 @@ class InboundPdaWorkflowServiceTest {
ReflectionTestUtils.setField(service, "unloadCompletedOutbox", unloadCompletedOutbox);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "unloadPhotoFiles", unloadPhotoFiles);
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
service.initializeObjectMapper();
plan = plan();
@@ -189,6 +193,20 @@ class InboundPdaWorkflowServiceTest {
assertEquals(3, targetPallet.getActualPackageCount());
}
@Test
void cartonScanRejectsBatchQuantityAndRequiresOneRealCartonPerScan() {
InboundPlanCarton first = carton("BOX-BATCH-1", "UNASSIGNED", targetLine.getId(), "WH-A");
InboundPlanCarton second = carton("BOX-BATCH-2", "UNASSIGNED", targetLine.getId(), "WH-A");
second.setId(42L);
activeTask();
when(repository.lockCarton("100", "BOX-BATCH-1")).thenReturn(first);
assertThrows(ResponseStatusException.class, () -> service.scanCarton("100", "LIP1",
new PdaRequests.ScanCarton("BOX-BATCH-1", null, 2, "scan-batch", "7", "worker")));
assertEquals("UNASSIGNED", first.getStatus());
assertEquals("UNASSIGNED", second.getStatus());
}
@Test
void cartonScanRejectsDuplicateAndWrongDestination() {
InboundPlanCarton repeated = carton("BOX-002", "ON_PALLET", targetLine.getId(), "WH-A");
@@ -225,7 +243,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void countMustPrecedePickAndPickedBoxesDriveActualVolume() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
target.setCountedPackageCount(0);
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
@@ -248,7 +268,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void overPickReturnsConfirmationWithoutChangingAllocationOrPallet() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
candidateQueries();
@@ -459,8 +481,95 @@ class InboundPdaWorkflowServiceTest {
verify(repository, never()).update(task);
}
@Test
void partialCompleteStocksOnlyActualPickedPackagesAndKeepsPartialStatus() {
target.setPickedPackageCount(2);
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
when(repository.cartons(plan.getId())).thenReturn(List.of());
var result = service.partialComplete("100", "LIP1",
new PdaRequests.ForceComplete("partial-1", "7", "worker"));
assertEquals("PARTIAL_INBOUND", result.status());
assertEquals(5, result.expectedPackageCount());
assertEquals(2, result.pickedPackageCount());
assertEquals("PARTIAL_INBOUND", plan.getStatus());
verify(stockReceipts).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(targetLine)),
eq(List.of(target)), eq(List.of(targetPallet)), any(), eq(true));
verify(unloadCompletedOutbox).publish(eq(plan), eq(task), eq(List.of(101L)));
}
@Test
void partialCompleteRequiresUnloadPhotosBeforeStockIn() {
target.setPickedPackageCount(2);
task.setUnloadPhotoFileIds(null);
task.setUnloadPhotoUploadedAt(null);
task.setEstimatedEmptyReturnAt(null);
activeTask();
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.partialComplete("100", "LIP1",
new PdaRequests.ForceComplete("partial-no-photo", "7", "worker")));
assertEquals(409, exception.getStatusCode().value());
verify(stockReceipts, never()).stockIn(any(), any(), any(), any(), any(), any(), any(), eq(true));
verify(unloadCompletedOutbox, never()).publish(any(), any(), any());
}
@Test
void dropshipTaskIsMarkedAndWholePalletPickReceivesThePlannedPalletAtOnce() {
plan.setSourceOrderId(101L);
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
when(repository.order(101L)).thenReturn(order);
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(
new DropshipPalletSnapshot("PLT-21", 3,
List.of(new DropshipPalletSnapshot.Sku("SKU-A", "Product A", 3, 30)))));
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
when(repository.task("100", plan.getId())).thenReturn(task);
activeTask();
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
var taskResult = service.task("100", "LIP1");
var pickResult = service.pickWholePallet("100", "LIP1", target.getId(),
new PdaRequests.WholePalletOperation("whole-1", "7", "worker"));
assertEquals("ONE_CLICK_DROPSHIP", taskResult.orderType());
assertTrue(taskResult.wholePalletInbound());
assertEquals("SKU-A", taskResult.dropshipPallets().get(0).skus().get(0).skuCode());
assertEquals(targetPallet.getId(), taskResult.dropshipPallets().get(0).planPalletId());
assertEquals(target.getId(), taskResult.dropshipPallets().get(0).allocationId());
assertEquals("WHOLE_PALLET_PICKED", pickResult.result());
assertEquals(3, target.getPickedPackageCount());
assertEquals(3, targetPallet.getActualPackageCount());
}
@Test
void dropshipPlanRejectsOrdinaryPackagePicking() {
plan.setSourceOrderId(101L);
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
when(repository.order(101L)).thenReturn(order);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.pick("100", "LIP1", target.getId(),
new PdaRequests.PackageOperation(1, "wrong-mode", "7", "worker")));
assertEquals(409, exception.getStatusCode().value());
verify(repository, never()).lockAllocation(plan.getId(), target.getId());
}
@Test
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
targetLine.setFbaWarehouseCode("ONT8");
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
InboundPdaOperation existing = new InboundPdaOperation();
existing.setAction("COUNT");
existing.setRequestHash("different");
@@ -472,7 +581,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void completedTaskStillReplaysAnAlreadyPersistedPick() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
@@ -19,6 +19,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -157,6 +158,21 @@ class InboundStockReceiptServiceTest {
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
}
@Test
void stocksInToHeldInventoryWhenInboundOrderIsDetained() {
Fixture fixture = fixture(5, 5, 5, 10);
fixture.order.setDetained(true);
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
verify(inventory).stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class));
verify(inventory, never()).stockIn(any(), any(), any());
}
@Test
void existingTaskReceiptMakesStockInReplayANoOp() {
Fixture fixture = fixture(10, 5, 5, 10);
@@ -200,6 +216,7 @@ class InboundStockReceiptServiceTest {
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
Fixture fixture = fixture(5, 5, 5, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
@@ -227,6 +244,45 @@ class InboundStockReceiptServiceTest {
verify(receipts, never()).insert(any(InboundStockReceipt.class));
}
@Test
void partialStockInSynchronizesOrderWithoutActivatingDropship() {
Fixture fixture = fixture(5, 2, 2, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 2)));
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 2, 0, 0, 0, false, false));
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true);
assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus());
assertEquals("INBOUND_PLAN:PARTIAL_INBOUND", fixture.order.getSubOrderStatus());
verify(orders).updateById(fixture.order);
verify(orderHistories).insert(any(LastlegOrderStatusHistory.class));
verify(dropshipOrders, never()).activateAfterStockIn(any(), any(), any(), any());
}
@Test
void rejectsPartiallyPickedDropshipSourcePallet() {
Fixture fixture = fixture(5, 5, 2, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true));
assertEquals(409, exception.getStatusCode().value());
verify(inventory, never()).stockIn(any(), any(), any());
}
private DropshipPalletSnapshot dropshipPallet(String palletNo, long packages) {
return new DropshipPalletSnapshot(palletNo, packages,
List.of(new DropshipPalletSnapshot.Sku("SKU-1", "Product", packages, packages * 10)));
}
private Fixture fixture(int required, int planned, int picked, int quantity) {
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1"); plan.setOverseasWarehouseId(900L);
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
@@ -502,6 +502,20 @@ class ReceivingContainerServiceBehaviorTest {
verifyNoInteractions(workflow, operations);
}
@Test
void cartonResolutionReturnsInboundAndPalletMergeReminderFromTailLegOrder() {
stubCurrent("FBA", "CONT-A");
LastlegOrder marked = order("FBA");
marked.setPalletMergeFlag("REQUIRED");
when(orders.selectById(1L)).thenReturn(marked);
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-1"));
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.palletMergeRequired()).isTrue();
assertThat(resolved.palletMergeHint()).contains("合托");
}
@Test
void fbaCartonResolutionRejectsCrossContainerAndInvalidPrefixesAsConflicts() {
stubCurrent("FBA", "CONT-A");