This commit is contained in:
2026-10-16 16:18:28 +08:00
parent 000125c3ec
commit 488c0318ff
13 changed files with 417 additions and 48 deletions
@@ -15,6 +15,7 @@ import static org.mockito.Mockito.verify;
class MerchantWarehouseTransferControllerTest {
private final WarehouseTransferService service = mock(WarehouseTransferService.class);
private final com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService forecastService = mock(com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService.class);
private final MerchantWarehouseTransferController controller = controller();
private final WarehouseTransferRequests.Query query = new WarehouseTransferRequests.Query(
1, 20, null, null, null, null, null, null, null, null);
@@ -50,6 +51,15 @@ class MerchantWarehouseTransferControllerTest {
verify(service).cartonLabel("100", "TR001");
}
/** 补预报只使用登录客户身份,平台身份不可越权操作。 */
@Test
void forecastIncreaseUsesAuthenticatedCustomerAndRejectsPlatform() {
var request = new WarehouseTransferRequests.IncreaseForecastCartons(2,9L,2,3);
controller.increaseForecastCartons(user(100L,"MERCHANT"),"TR001",request);
verify(forecastService).increase("100","TR001","1",request);
assertThrows(ResponseStatusException.class,()->controller.increaseForecastCartons(user(0L,"PLATFORM"),"TR001",request));
}
@Test
void merchantEndpointRejectsPlatformIdentity() {
assertThrows(ResponseStatusException.class,
@@ -63,6 +73,7 @@ class MerchantWarehouseTransferControllerTest {
private MerchantWarehouseTransferController controller() {
MerchantWarehouseTransferController controller = new MerchantWarehouseTransferController();
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "forecastService", forecastService);
return controller;
}
}
@@ -37,26 +37,61 @@ class WarehouseTransferCartonCountTest {
a = box("BOX1", 10L); b = box("BOX2", 11L);
when(orders.lockForPda("WT1", "77")).thenReturn(order);
when(cartons.listOrder("C1", 1L)).thenReturn(List.of(a,b));
var ia = new WarehouseTransferItem(); ia.setId(10L); ia.setSkuCode("A");
var ib = new WarehouseTransferItem(); ib.setId(11L); ib.setSkuCode("B");
var ia = new WarehouseTransferItem(); ia.setId(10L); ia.setSkuCode("A"); ia.setPackageCount(1); ia.setDeclaredQuantity(4L);
var ib = new WarehouseTransferItem(); ib.setId(11L); ib.setSkuCode("B"); ib.setPackageCount(1); ib.setDeclaredQuantity(0L);
when(items.selectList(any())).thenReturn(List.of(ia,ib));
}
WarehouseTransferCarton box(String mark, long item) { var box = new WarehouseTransferCarton(); box.setCartonMark(mark); box.setCartonIndex((int)item); box.setTransferItemId(item); return box; }
WarehouseTransferRequests.CountCarton input(String mark, String sku, long quantity) { return new WarehouseTransferRequests.CountCarton(mark, null, 200L, new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),List.of(new WarehouseTransferRequests.CountSku(sku,quantity))); }
WarehouseTransferRequests.CountCartons request(String key, WarehouseTransferRequests.CountCarton... rows) { return new WarehouseTransferRequests.CountCartons(key,List.of(rows),null); }
WarehouseTransferRequests.CountCartons request(String key, WarehouseTransferRequests.CountCarton... rows) { return new WarehouseTransferRequests.CountCartons(key,List.of(rows),null,2); }
@Test void multiSkuBoxesPersistDimensionsPiecesAndReplayWithoutWriting() {
var request = request("K1",input("BOX1","A",12),input("BOX2","B",7));
service.countCartons("OP1",77L,"WT1",request);
assertEquals(12L,a.getCountedQuantity()); assertEquals(7L,b.getCountedQuantity()); assertNotNull(a.getCountedAt()); assertEquals("OP1",a.getCountedBy()); assertEquals(new BigDecimal("2.5"),b.getGrossWeightKg());
assertEquals(200L,a.getTransferPalletId()); assertEquals(2,pallet.getCartonCount());
assertEquals(new BigDecimal("0.048000"),pallet.getVolumeCbm()); assertEquals(new BigDecimal("5.0"),pallet.getGrossWeightKg());
assertEquals(new BigDecimal("0.048"),pallet.getVolumeCbm()); assertEquals(new BigDecimal("5.0"),pallet.getGrossWeightKg());
clearInvocations(cartons); order.setStatus("WAIT_CUSTOMER_CONFIRMATION");
service.countCartons("OP1",77L,"WT1",request);
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K1",input("BOX1","A",13),input("BOX2","B",7))));
}
/** 整托汇总后再保留三位,不能把每箱先舍入后累加。 */
@Test void volumeRoundsOnceAfterSummingBoxes() {
var first = new WarehouseTransferRequests.CountCarton("BOX1", null, 200L,
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("6"), new BigDecimal("2.5"),
List.of(new WarehouseTransferRequests.CountSku("A", 3)));
var second = new WarehouseTransferRequests.CountCarton("BOX2", null, 200L,
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("6"), new BigDecimal("2.5"),
List.of(new WarehouseTransferRequests.CountSku("B", 4)));
service.countCartons("OP1", 77L, "WT1", request("ROUND", first, second));
assertEquals(new BigDecimal("0.001"), pallet.getVolumeCbm());
assertEquals(new BigDecimal("6"), a.getHeightCm());
assertEquals(new BigDecimal("5.0"), pallet.getGrossWeightKg());
}
@Test void remeasurementInvalidatesCountAndRejectsCompletedOrForeignTransfers() {
service.reopenMeasurement(77L, "WT1");
verify(cartons).reopenMeasurement("C1", 1L);
assertThrows(ResponseStatusException.class, () -> service.reopenMeasurement(88L, "WT1"));
order.setStatus("WAIT_CUSTOMER_CONFIRMATION");
assertThrows(ResponseStatusException.class, () -> service.reopenMeasurement(77L, "WT1"));
verify(cartons, times(1)).reopenMeasurement("C1", 1L);
}
@Test void deletingBoundPalletUnassignsBoxesAndReleasesResource() {
var resources = mock(com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway.class);
ReflectionTestUtils.setField(service, "warehouseResources", resources);
pallet.setStatus("BOUND"); pallet.setWarehouseResourceLockId(901L);
when(palletMapper.lockOne("C1", 1L, 200L)).thenReturn(pallet);
service.deletePalletDraft(77L, "WT1", 200L);
verify(resources).release("C1", 901L);
verify(cartons).unassignPallet("C1", 1L, 200L);
verify(cartons).reopenMeasurement("C1", 1L);
verify(palletMapper).deleteById(200L);
}
@Test void missingDuplicateForeignSkuAndInvalidQuantityRejectBeforeWrites() {
var invalid = List.of(request("K",input("BOX1","A",1)),request("K",input("BOX1","A",1),input("BOX1","A",2)),request("K",input("OTHER","A",1),input("BOX2","B",2)),request("K",input("BOX1","FOREIGN",1),input("BOX2","B",2)),request("K",input("BOX1","A",0),input("BOX2","B",2)));
var invalid = List.of(request("K",input("BOX1","A",1),input("BOX1","A",2)),request("K",input("OTHER","A",1),input("BOX2","B",2)),request("K",input("BOX1","FOREIGN",1),input("BOX2","B",2)),request("K",input("BOX1","A",0),input("BOX2","B",2)));
for(var request:invalid) assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request));
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
}
@@ -64,14 +99,14 @@ class WarehouseTransferCartonCountTest {
var mixed = new WarehouseTransferRequests.CountCarton("BOX1", null, 200L,
new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),
List.of(new WarehouseTransferRequests.CountSku("A",3),new WarehouseTransferRequests.CountSku("B",7)));
var request = new WarehouseTransferRequests.CountCartons("MIX",List.of(mixed),"两种SKU装一箱");
var request = new WarehouseTransferRequests.CountCartons("MIX",List.of(mixed),"两种SKU装一箱",2);
service.countCartons("OP1",77L,"WT1",request);
assertEquals(10L,a.getCountedQuantity());
var contents = new ObjectMapper().readTree(a.getSkuItemsJson());
assertEquals(2,contents.size()); assertEquals("A",contents.get(0).get("skuCode").asText());
assertEquals(7,contents.get(1).get("quantity").asInt());
assertEquals(11L,contents.get(1).get("transferItemId").asLong());
assertEquals(1,pallet.getCartonCount()); assertEquals(new BigDecimal("0.024000"),pallet.getVolumeCbm());
assertEquals(1,pallet.getCartonCount()); assertEquals(new BigDecimal("0.024"),pallet.getVolumeCbm());
assertEquals(new BigDecimal("2.5"),pallet.getGrossWeightKg());
var allocation = WarehouseTransferService.deriveCountedPalletAllocations(List.of(a),List.of(pallet));
assertEquals(1,allocation.size()); assertEquals(1,allocation.get(0).packageCount());
@@ -83,7 +118,7 @@ class WarehouseTransferCartonCountTest {
var changed = new WarehouseTransferRequests.CountCarton("BOX1",null,200L,mixed.length(),mixed.width(),mixed.height(),mixed.weight(),
List.of(new WarehouseTransferRequests.CountSku("A",4),new WarehouseTransferRequests.CountSku("B",6)));
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",
new WarehouseTransferRequests.CountCartons("MIX",List.of(changed),"两种SKU装一箱")));
new WarehouseTransferRequests.CountCartons("MIX",List.of(changed),"两种SKU装一箱",2)));
}
@Test void duplicateUnknownEmptyAndInvalidSkuLinesRejectBeforeAnyWrites() {
var invalidLines = List.of(List.<WarehouseTransferRequests.CountSku>of(),
@@ -98,17 +133,48 @@ class WarehouseTransferCartonCountTest {
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
}
@Test void shortReceiptNeedsReasonAndPreservesMissingBoxFact() {
var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱");
var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱",2);
service.countCartons("OP1",77L,"WT1",request);
assertEquals("COUNTED",a.getStatus()); assertEquals("NOT_RECEIVED",b.getStatus());
assertEquals("少到一箱",b.getDiscrepancyReason()); assertNull(b.getCountedQuantity());
}
@Test void extraReceiptGeneratesOwnedBoxAndReconcilesToDeclaredSku() {
/** 短收可先清点,最终完成必须另行提供非空原因。 */
@Test void shortReceiptCanCountWithoutReasonButCannotComplete() {
service.countCartons("OP1",77L,"WT1",request("SHORT-NO-REASON",input("BOX1","A",3)));
assertEquals("COUNTED", a.getStatus()); assertEquals("NOT_RECEIVED", b.getStatus());
order.setOperatingWarehouseId("77");
clearInvocations(cartons);
for (String reason : new String[] {"", " "}) {
var error = assertThrows(ResponseStatusException.class, () -> service.palletize("OP1",77L,"WT1",
new WarehouseTransferRequests.Palletize("DONE",List.of(9001L),reason)));
assertTrue(error.getReason().contains("必须填写差异原因"));
}
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
assertEquals("WAIT_PDA_RECEIVING", order.getStatus());
}
/** 相同总件数下各SKU一短一超仍有差异,改变箱数但SKU核平则无差异。 */
@Test void skuReceiptDifferenceDoesNotNetDifferentSkusOrUseBoxCount() {
var ia = new WarehouseTransferItem(); ia.setId(10L); ia.setSkuCode("A"); ia.setDeclaredQuantity(3L);
var ib = new WarehouseTransferItem(); ib.setId(11L); ib.setSkuCode("B"); ib.setDeclaredQuantity(7L);
a.setSkuItemsJson("[{\"transferItemId\":10,\"skuCode\":\"A\",\"quantity\":2},{\"transferItemId\":11,\"skuCode\":\"B\",\"quantity\":8}]");
assertTrue(service.hasSkuReceiptDifference(List.of(a),List.of(ia,ib)));
a.setSkuItemsJson("[{\"transferItemId\":10,\"skuCode\":\"A\",\"quantity\":3},{\"transferItemId\":11,\"skuCode\":\"B\",\"quantity\":7}]");
assertFalse(service.hasSkuReceiptDifference(List.of(a),List.of(ia,ib)));
ia.setDeclaredQuantity(4L);
assertTrue(service.hasSkuReceiptDifference(List.of(a),List.of(ia,ib)));
}
/** 新箱必须先补客户预报,PDA不能绕过预报生成另一套箱号。 */
@Test void extraReceiptRequiresForecastCartonFirst() {
var extra = new WarehouseTransferRequests.CountCarton(null,11L,200L,new BigDecimal("10"),new BigDecimal("20"),new BigDecimal("30"),BigDecimal.ONE,List.of(new WarehouseTransferRequests.CountSku("B",4)));
service.countCartons("OP1",77L,"WT1",new WarehouseTransferRequests.CountCartons("EXTRA",List.of(input("BOX1","A",1),input("BOX2","B",2),extra),"多到一箱"));
var captured = org.mockito.ArgumentCaptor.forClass(WarehouseTransferCarton.class);
verify(cartons).insert(captured.capture());
assertEquals("WT1#EX-12",captured.getValue().getCartonMark()); assertEquals("C1",captured.getValue().getCustomerId()); assertEquals(4L,captured.getValue().getCountedQuantity());
assertThrows(ResponseStatusException.class, () -> service.countCartons("OP1",77L,"WT1",new WarehouseTransferRequests.CountCartons("EXTRA",List.of(input("BOX1","A",1),input("BOX2","B",2),extra),null,2)));
verify(cartons,never()).insert(any(WarehouseTransferCarton.class));
}
/** 旧预报版本不能把客户刚新增的箱唛标为未收到。 */
@Test void staleForecastCannotCount() {
order.setPackageCount(3);
var error = assertThrows(ResponseStatusException.class, () -> service.countCartons("OP1",77L,"WT1",request("STALE",input("BOX1","A",1))));
assertTrue(error.getReason().contains("预报箱数已变化"));
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
}
@Test void cartonsRequireGeneratedPalletOwnedByThisOrder() {
when(palletMapper.listOrder("C1",1L)).thenReturn(List.of());
@@ -208,6 +208,9 @@ class WarehouseTransferServiceTest {
void setUp() {
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.CompactBusinessNumberService.class);
service = new WarehouseTransferService();
var measurementRepository = new com.baiyue.lastleg.transfer.repository.TransferMeasurementRepository();
ReflectionTestUtils.setField(measurementRepository, "cartons", transferCartons);
ReflectionTestUtils.setField(service, "transferMeasurementRepository", measurementRepository);
ReflectionTestUtils.setField(service, "compactNumbers", compactNumbers);
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
ReflectionTestUtils.setField(service, "customerDirectory", customerDirectory);
@@ -327,7 +330,7 @@ class WarehouseTransferServiceTest {
transfer.setOperatingWarehouseId("77");
WarehouseTransferPallet pallet = measuredPallet("GENERATED");
var counted = java.util.stream.IntStream.rangeClosed(1, 5).mapToObj(index -> {
var box = new WarehouseTransferCarton(); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
var box = new WarehouseTransferCarton(); box.setTransferItemId(51L); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
}).toList();
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(counted);
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
@@ -344,13 +347,16 @@ class WarehouseTransferServiceTest {
verify(warehouseResources, never()).ensurePalletBatch(any());
}
@Test
void palletizeCompletesAfterEveryPalletIsBoundAndDoesNotGenerateAgain() {
/** 有无短收均可完成,差异原因应随完成事务保存并支持幂等重放。 */
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(booleans = {false, true})
void palletizeCompletesAfterEveryPalletIsBoundAndDoesNotGenerateAgain(boolean discrepancy) {
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
transfer.setOperatingWarehouseId("77");
transfer.setInboundCode("WTR-1");
WarehouseTransferPallet pallet = measuredPallet("BOUND");
WarehouseTransferItem item = transferItem(51L, "SKU-1", 5);
transfer.setPackageCount(6); item.setPackageCount(6); item.setDeclaredQuantity(discrepancy ? 6L : 5L);
LastlegOrder tail = tailOrder();
LastlegInboundOrder inbound = new LastlegInboundOrder();
inbound.setId(transfer.getInboundOrderId()); inbound.setCustomerId("TENANT-A");
@@ -375,11 +381,21 @@ class WarehouseTransferServiceTest {
when(sourceOrders.selectList(any())).thenReturn(List.of());
var counted = java.util.stream.IntStream.rangeClosed(1, 5).mapToObj(index -> {
var box = new WarehouseTransferCarton(); box.setTransferPalletId(pallet.getId()); box.setTransferItemId(item.getId()); box.setCartonIndex(index); box.setTotalCartons(5); box.setCartonMark("BOX-" + index); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
var box = new WarehouseTransferCarton(); box.setTransferPalletId(pallet.getId()); box.setTransferItemId(item.getId()); box.setCartonIndex(index); box.setTotalCartons(5); box.setCountedQuantity(1L); box.setCartonMark("BOX-" + index); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
}).toList();
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(counted);
var result = service.palletize("OP-1", 77L, "WTR-1",
new WarehouseTransferRequests.Palletize("DONE-1", List.of(9001L)));
var request = new WarehouseTransferRequests.Palletize("DONE-1", List.of(9001L), discrepancy ? " 客户少发一箱 " : null);
when(transferCartons.saveDiscrepancyReason(eq("TENANT-A"), eq(transfer.getId()), any())).thenAnswer(call -> {
counted.forEach(box -> box.setDiscrepancyReason(call.getArgument(2)));
return counted.size();
});
var result = service.palletize("OP-1", 77L, "WTR-1", request);
assertTrue(counted.stream().allMatch(box -> java.util.Objects.equals(discrepancy ? "客户少发一箱" : null, box.getDiscrepancyReason())));
assertTrue(result.cartons().stream().allMatch(box -> java.util.Objects.equals(discrepancy ? "客户少发一箱" : null, box.discrepancyReason())));
clearInvocations(transferCartons);
service.palletize("OP-1", 77L, "WTR-1", request);
verify(transferCartons, never()).updateById(any(WarehouseTransferCarton.class));
verify(transferCartons, never()).saveDiscrepancyReason(anyString(), anyLong(), any());
assertEquals(WarehouseTransferService.WAIT_CUSTOMER_CONFIRMATION, result.status());
assertEquals("COMPLETED", pallet.getStatus());
@@ -787,6 +803,21 @@ class WarehouseTransferServiceTest {
verify(transferCartons, never()).listOrder(anyString(), anyLong());
}
/** 提交预报后即可打印全部预报箱唛,不依赖测量或照片完成。 */
@Test
void submittedForecastLabelsArePrintableBeforeCounting() {
var transfer = transfer(WarehouseTransferService.PENDING_REVIEW);
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
var box = new WarehouseTransferCarton(); box.setCartonMark("FORECAST-BOX"); box.setCartonIndex(1); box.setTotalCartons(2); box.setStatus("EXPECTED");
var missing = new WarehouseTransferCarton(); missing.setCartonMark("FORECAST-BOX-2"); missing.setCartonIndex(2); missing.setTotalCartons(2); missing.setStatus("NOT_RECEIVED");
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(box, missing));
var label = service.cartonLabel("TENANT-A", "WTR-1");
assertEquals(2, label.cartons().size());
assertTrue(label.printableHtml().contains("FORECAST-BOX"));
assertNull(box.getCountedAt());
verify(inventory, never()).stockIn(anyString(), anyString(), any());
}
@Test
void rejectionRequiresNonBlankReasonBeforeAnyStateOrInventoryChange() {
for (String decision : List.of("REJECT", "REJECTED")) {