fix(lastleg): validate dropship SKU for PDA outbound
This commit is contained in:
+33
-13
@@ -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 {
|
||||
|
||||
+46
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user