fix(lastleg): validate dropship SKU for PDA outbound

This commit is contained in:
2026-09-02 12:09:14 +08:00
parent 08be508501
commit 1574263836
2 changed files with 79 additions and 13 deletions
@@ -217,9 +217,7 @@ public class DeliveryTransferService {
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
LocalDateTime now = LocalDateTime.now();
InventoryPosition firstPosition = inventoryPositions.selectById(request.items().get(0).inventoryPositionId());
if (firstPosition == null || !customerId.equals(firstPosition.getCustomerId())) throw notFound("Inventory position not found");
String selectedWarehouseId = firstPosition.getOverseasWarehouseId();
String selectedWarehouseId = validateOneClickItems(customerId, request.items());
OutboundOrder outbound = new OutboundOrder();
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP", selectedWarehouseId, outbound.getId()));
@@ -230,11 +228,6 @@ public class DeliveryTransferService {
for (DeliveryRequests.OneClickItem input : request.items()) {
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found");
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
if (profile == null) throw conflict("Selected pallet is not one-click dropship inventory");
if (selectedWarehouseId == null) selectedWarehouseId = position.getOverseasWarehouseId();
else if (!Objects.equals(selectedWarehouseId, position.getOverseasWarehouseId())) throw conflict("One outbound order must use inventory from the same overseas warehouse");
if (value(position.getAvailablePackages()) < input.packages()) throw conflict("Insufficient available inventory for pallet " + position.getPalletNo());
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId());
item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCreatedAt(now);
outboundPallets.insert(item);
@@ -245,6 +238,37 @@ public class DeliveryTransferService {
return taskResult(outbound, task);
}
private String validateOneClickItems(String customerId, List<DeliveryRequests.OneClickItem> items) {
String warehouseId = null;
Map<String, Long> requestedPackages = new LinkedHashMap<>();
Map<String, DropshipOrderProfile> profiles = new HashMap<>();
for (DeliveryRequests.OneClickItem input : items) {
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found");
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
if (profile == null) throw conflict("Selected pallet is not one-click dropship inventory");
if (warehouseId == null) warehouseId = position.getOverseasWarehouseId();
else if (!Objects.equals(warehouseId, position.getOverseasWarehouseId())) {
throw conflict("One outbound order must use inventory from the same overseas warehouse");
}
if (value(position.getAvailablePackages()) < input.packages()) {
throw conflict("Insufficient available inventory for pallet " + position.getPalletNo());
}
String key = profile.getId() + "\u0000" + input.sku().trim();
requestedPackages.merge(key, input.packages(), Long::sum);
profiles.put(key, profile);
}
for (Map.Entry<String, Long> requested : requestedPackages.entrySet()) {
String sku = requested.getKey().substring(requested.getKey().indexOf('\u0000') + 1);
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId,
profiles.get(requested.getKey()).getId(), sku, requested.getValue());
if (cartons.size() != requested.getValue()) {
throw conflict("Insufficient available one-click dropship cartons for SKU " + sku);
}
}
return warehouseId;
}
@Transactional
public DeliveryResponses.PdaTask pdaPick(String warehouseId, String operatorId, String taskNo,
DeliveryRequests.PdaPick request) {
@@ -278,10 +302,6 @@ public class DeliveryTransferService {
if (!warehouseId.equals(selectedPosition.getOverseasWarehouseId())) {
throw conflict("Scanned inventory position does not belong to the bound warehouse");
}
InventoryBalance selectedBalance = inventoryBalances.selectById(selectedPosition.getBalanceId());
if (!matchesOutboundItem(customerId, warehouseId, item, selectedBalance)) {
throw conflict("Scanned inventory position does not belong to the outbound item");
}
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, request.inventoryPositionId());
if (profile == null) throw conflict("Scanned pallet is not one-click dropship inventory");
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
@@ -395,12 +415,12 @@ public class DeliveryTransferService {
long available = value(position.getAvailablePackages());
if (available <= 0 || !warehouseId.equals(position.getOverseasWarehouseId())) continue;
InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId());
if (!matchesOutboundItem(customerId, warehouseId, item, balance)) continue;
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
List<String> marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId())
.stream().filter(carton -> "AVAILABLE".equals(carton.getStatus()))
.filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode()))
.map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList();
if (!matchesOutboundItem(customerId, warehouseId, item, balance) && marks.isEmpty()) continue;
if (marks.isEmpty()) {
result.add(pdaPosition(position, item, balance, null, available, value(position.getPickedPackages())));
} else {
@@ -356,6 +356,24 @@ class DeliveryTransferServiceTest {
verify(outboundPallets, never()).findByOutbound(any(), any());
}
@Test
void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() {
InventoryPosition position = new InventoryPosition();
position.setId(901L); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
position.setPalletId(501L); position.setPalletNo("PLT-501"); position.setAvailablePackages(1L);
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "MIXED", 1L)).thenReturn(List.of());
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick(
"TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of(
new DeliveryRequests.OneClickItem(901L, 1L, "MIXED")), null, "one-click-mixed")));
assertEquals(409, exception.getStatusCode().value());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
}
@Test
void outboundPdaListAndDetailUseOnlyBoundWarehouse() throws Exception {
assertNotNull(DeliveryTransferService.class.getMethod("pdaPick", String.class, String.class,
@@ -415,6 +433,34 @@ class DeliveryTransferServiceTest {
assertEquals(1L, projected.availablePackages());
}
@Test
void outboundPdaDetailUsesCartonMarksWhenInventoryBalanceIsMixed() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");
outbound.setOverseasWarehouseId("WH-1");
OutboundPdaTask task = new OutboundPdaTask();
task.setId(701L); task.setCustomerId("TENANT-A"); task.setOutboundOrderId(301L); task.setTaskNo("PDA-701");
OutboundPalletItem item = pallet(501L, "PLT-501", "ONT8", "Ontario", null, 1);
item.setSkuCode("SKU-1");
InventoryPosition position = new InventoryPosition();
position.setId(901L); position.setCustomerId("TENANT-A"); position.setBalanceId(801L);
position.setOverseasWarehouseId("WH-1"); position.setPalletId(501L); position.setPalletNo("PLT-501"); position.setAvailablePackages(1L);
InventoryBalance balance = new InventoryBalance();
balance.setId(801L); balance.setCustomerId("TENANT-A"); balance.setOverseasWarehouseId("WH-1"); balance.setSkuCode("MIXED");
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
DropshipCarton carton = new DropshipCarton(); carton.setStatus("AVAILABLE"); carton.setSkuCode("SKU-1"); carton.setCartonMark("CTN-A");
when(pdaTasks.findByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(inventoryPositions.listPallet("TENANT-A", 501L)).thenReturn(List.of(position));
when(inventoryBalances.selectById(801L)).thenReturn(balance);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.listProfile("TENANT-A", 1001L)).thenReturn(List.of(carton));
DeliveryResponses.PdaTaskDetail detail = service.pdaTaskDetail("WH-1", "PDA-701");
assertEquals("CTN-A", detail.items().get(0).positions().get(0).cartonMark());
}
@Test
void outboundPdaPickReplayDoesNotDeductInventoryAgainAndRejectsChangedRequest() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");