fix(lastleg): create remaining container plans
This commit is contained in:
+1
-1
@@ -61,7 +61,7 @@ public class InboundPlanController {
|
||||
|
||||
@PostMapping("/convert-container")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
|
||||
@Operation(summary = "按整柜批量生成入库计划", description = "平台按 sourceContainerBookingId 锁定整柜订单;每个入仓码独立生成计划,整批原子且可幂等重放。Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
|
||||
@Operation(summary = "按整柜批量生成剩余入库计划", description = "平台按 sourceContainerBookingId 锁定整柜订单;保留已有有效计划,只为剩余入仓码分别生成独立计划,整批原子且可幂等重放。Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
|
||||
public ApiResponse<java.util.List<InboundPlanResponse>> convertContainer(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ConvertContainerInboundPlanRequest request) {
|
||||
|
||||
+39
-27
@@ -285,26 +285,24 @@ public class InboundPlanService {
|
||||
List<InboundPlan> existingPlans = refs.stream()
|
||||
.map(ref -> planMapper.findBySourceOrder(ref.customerId(), ref.orderId()))
|
||||
.toList();
|
||||
long existingCount = existingPlans.stream().filter(Objects::nonNull).count();
|
||||
if (existingCount == refs.size()) {
|
||||
List<InboundPlanResponse> replay = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
requirePlanContainer(existingPlans.get(index), containerNo);
|
||||
replay.add(detail(refs.get(index).customerId(), existingPlans.get(index)));
|
||||
List<String> warehouseIds = new ArrayList<>(refs.size());
|
||||
List<Integer> pendingIndexes = new ArrayList<>();
|
||||
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>();
|
||||
List<List<WholePalletInput>> wholePallets = new ArrayList<>();
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
InboundPlan existing = existingPlans.get(index);
|
||||
if (existing != null) {
|
||||
requireValidExistingPlan(existing, containerNo);
|
||||
InboundPlanOrderMapper.ReceivingWarehouseSnapshot warehouse =
|
||||
orderMapper.findReceivingWarehouse(ref.customerId(), ref.orderId());
|
||||
warehouseIds.add(warehouse == null ? null : warehouse.overseasWarehouseId());
|
||||
continue;
|
||||
}
|
||||
if (!("PENDING_ASSIGNMENT".equals(ref.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在不能转换的尾程订单");
|
||||
}
|
||||
return List.copyOf(replay);
|
||||
}
|
||||
if (existingCount != 0) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜仅部分尾程订单已有入库计划");
|
||||
}
|
||||
if (refs.stream().anyMatch(ref -> !("PENDING_ASSIGNMENT".equals(ref.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus())))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在不能转换的尾程订单");
|
||||
}
|
||||
|
||||
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>(refs.size());
|
||||
List<List<WholePalletInput>> wholePallets = new ArrayList<>(refs.size());
|
||||
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(ref.customerId(), ref.orderId());
|
||||
if (orders == null || orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜订单在锁定后不存在");
|
||||
@@ -314,7 +312,9 @@ public class InboundPlanService {
|
||||
if (!Objects.equals(containerNo, currentContainerNo)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
|
||||
}
|
||||
pendingIndexes.add(index);
|
||||
snapshots.add(orders);
|
||||
warehouseIds.addAll(orders.stream().map(SourceOrderSnapshot::overseasWarehouseId).toList());
|
||||
List<WholePalletInput> sourcePallets = "ONE_CLICK_DROPSHIP".equals(ref.orderType())
|
||||
? dropshipOrders.palletSnapshots(ref.customerId(), ref.orderId()).stream()
|
||||
.map(this::wholePallet).toList()
|
||||
@@ -328,16 +328,21 @@ public class InboundPlanService {
|
||||
}
|
||||
wholePallets.add(sourcePallets);
|
||||
}
|
||||
requireSingleWarehouseId(snapshots.stream().flatMap(List::stream)
|
||||
.map(SourceOrderSnapshot::overseasWarehouseId).toList());
|
||||
requireSingleWarehouseId(warehouseIds);
|
||||
|
||||
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(),
|
||||
wholePallets.get(index), null, null));
|
||||
Map<Integer, InboundPlanResponse> created = new HashMap<>();
|
||||
for (int pendingIndex = 0; pendingIndex < pendingIndexes.size(); pendingIndex++) {
|
||||
int refIndex = pendingIndexes.get(pendingIndex);
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(refIndex);
|
||||
created.put(refIndex, createPlan(ref.customerId(), ref.orderId(), snapshots.get(pendingIndex), List.of(),
|
||||
wholePallets.get(pendingIndex), null, null));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
List<InboundPlanResponse> result = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlan existing = existingPlans.get(index);
|
||||
result.add(existing == null ? created.get(index) : detail(refs.get(index).customerId(), existing));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private WholePalletInput wholePallet(DropshipPalletSnapshot pallet) {
|
||||
@@ -397,6 +402,13 @@ public class InboundPlanService {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireValidExistingPlan(InboundPlan plan, String containerNo) {
|
||||
requirePlanContainer(plan, containerNo);
|
||||
if (trimToNull(plan.getPlanNo()) == null || trimToNull(plan.getStatus()) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在无效入库计划,不能继续按柜生成");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateWholePallets(List<SourceOrderSnapshot> orders, List<WholePalletInput> pallets) {
|
||||
if (orders.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
|
||||
@@ -420,8 +420,8 @@ paths:
|
||||
'409': { description: 订单版本、状态、暂扣状态、重复计划或幂等键冲突 }
|
||||
/api/platform/v1/last-leg/inbound-plans/convert-container:
|
||||
post:
|
||||
summary: 按整柜批量生成入库计划
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:create;使用稳定 sourceContainerBookingId 锁定整柜订单,并要求全柜尾程订单及既有计划的真实 containerNo 快照一致。每个尾程订单/入仓码独立生成一个入库计划,整批原子且幂等;任何柜号漂移均冲突且不部分写入。
|
||||
summary: 按整柜批量生成剩余入库计划
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:create;使用稳定 sourceContainerBookingId 锁定整柜订单,并要求全柜尾程订单及既有有效计划的真实 containerNo 与接柜仓快照一致。已有有效计划保持不变,只为剩余尾程订单/入仓码分别生成独立计划;响应按柜内订单稳定顺序返回全部已有及新增计划。整批原子且幂等,剩余为0时直接返回已有结果;任何快照漂移均冲突且不部分写入。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody:
|
||||
required: true
|
||||
|
||||
+37
-3
@@ -524,16 +524,50 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||
void partialExistingPlansArePreservedWhileOnlyRemainingOrdersAreCreated() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(order("100", 2L, "RK-2", "WH-B")));
|
||||
when(orderMapper.markPlanned(eq("100"), eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
var result = service.convertContainer("BOOKING-1");
|
||||
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("LIP-1", result.get(0).planNo());
|
||||
assertEquals(2L, result.get(1).sourceOrderId());
|
||||
verify(orderMapper, never()).lockOrder("100", 1L);
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(eq("100"), eq(1L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(orderMapper).markPlanned(eq("100"), eq(2L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansWithMixedWarehouseFactsCauseZeroNewWrites() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200004", null, null, null, "WH-B", "WH-B", "仓B", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(differentWarehouse));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user