fix(lastleg): enforce container warehouse plan facts
This commit is contained in:
-4
@@ -11,11 +11,7 @@ public record ManualInboundPlanRequest(
|
||||
@NotBlank @Size(max = 128) String customerId,
|
||||
@NotNull Long orderId,
|
||||
@NotNull @Min(0) Integer expectedOrderVersion,
|
||||
@NotNull Long plannedWarehouseId,
|
||||
@NotBlank @Size(max = 128) String plannedWarehouseCode,
|
||||
@NotBlank @Size(max = 255) String plannedWarehouseName,
|
||||
@NotNull LocalDate plannedArrivalDate,
|
||||
@NotBlank @Size(max = 128) String inboundCode,
|
||||
@NotNull @Min(1) @Max(999) Integer palletCount,
|
||||
@NotBlank @Size(max = 32) String palletType,
|
||||
@NotNull Boolean assigned,
|
||||
|
||||
+11
@@ -16,6 +16,17 @@ public interface InboundPlanOrderMapper {
|
||||
String orderType) { }
|
||||
record ReceivingWarehouseSnapshot(String overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName) { }
|
||||
record ContainerWarehouseRef(String customerId, Long orderId, String sourceContainerBookingId,
|
||||
String overseasWarehouseId) { }
|
||||
|
||||
@Select("SELECT c.customer_id AS customerId,c.id AS orderId," +
|
||||
"c.source_container_booking_id AS sourceContainerBookingId,c.overseas_warehouse_id AS overseasWarehouseId " +
|
||||
"FROM ll_order target JOIN ll_order c ON c.source_container_booking_id=target.source_container_booking_id " +
|
||||
"WHERE target.customer_id=#{customerId} AND target.id=#{orderId} " +
|
||||
"AND target.source_container_booking_id IS NOT NULL AND target.source_container_booking_id!='' " +
|
||||
"ORDER BY c.customer_id,c.id FOR UPDATE")
|
||||
List<ContainerWarehouseRef> lockContainerWarehouseFacts(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
|
||||
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo," +
|
||||
"order_type AS orderType " +
|
||||
|
||||
+53
-20
@@ -75,6 +75,13 @@ public class InboundPlanService {
|
||||
if (key == null || key.length() > 128) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Idempotency-Key 必须为1至128个字符");
|
||||
}
|
||||
List<InboundPlanOrderMapper.ContainerWarehouseRef> containerFacts =
|
||||
orderMapper.lockContainerWarehouseFacts(customerId, request.orderId());
|
||||
if (containerFacts == null || containerFacts.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "尾程订单不存在或未绑定来源柜");
|
||||
}
|
||||
String containerWarehouseId = requireSingleWarehouseId(containerFacts.stream()
|
||||
.map(InboundPlanOrderMapper.ContainerWarehouseRef::overseasWarehouseId).toList());
|
||||
List<SourceOrderSnapshot> snapshots = orderMapper.lockOrder(customerId, request.orderId());
|
||||
if (snapshots == null || snapshots.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "尾程订单不存在");
|
||||
@@ -96,20 +103,14 @@ public class InboundPlanService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "尾程订单版本已变化,请刷新后重试");
|
||||
}
|
||||
if (order.detained()) throw new ResponseStatusException(HttpStatus.CONFLICT, "暂扣尾程订单不能生成入库计划");
|
||||
if (!Objects.equals(trimToNull(order.inboundCode()), trimToNull(request.inboundCode()))) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "入仓码与尾程订单冻结事实不一致");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference warehouse =
|
||||
warehouseGateway.requireActiveSharedWarehouse(request.plannedWarehouseId());
|
||||
if (!Objects.equals(String.valueOf(warehouse.warehouseId()), trimToNull(order.overseasWarehouseId()))
|
||||
|| !Objects.equals(warehouse.warehouseCode(), request.plannedWarehouseCode().trim())
|
||||
|| !Objects.equals(warehouse.warehouseName(), request.plannedWarehouseName().trim())
|
||||
|| !Objects.equals(warehouse.warehouseCode(), trimToNull(order.overseasWarehouseCode()))
|
||||
|| !Objects.equals(warehouse.warehouseName(), trimToNull(order.overseasWarehouseName()))) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "计划入仓仓库不存在、已停用或与订单预分配事实不一致");
|
||||
if (!Objects.equals(containerWarehouseId, trimToNull(order.overseasWarehouseId()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜接柜仓冻结事实已变化,请刷新后重试");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
|
||||
requiredReceivingWarehouse(containerWarehouseId);
|
||||
validateOrders(snapshots);
|
||||
InboundPlanResponse response = createPlan(customerId, request.orderId(), snapshots, List.of(), null, request);
|
||||
InboundPlanResponse response = createPlan(customerId, request.orderId(), snapshots, List.of(), null,
|
||||
request, receivingWarehouse);
|
||||
LastlegOrderCommandRecord command = new LastlegOrderCommandRecord();
|
||||
command.setId(IdWorker.getId()); command.setCustomerId(customerId); command.setOrderId(request.orderId());
|
||||
command.setAction("MANUAL_PLAN"); command.setOperatorId(operatorId); command.setIdempotencyKey(key);
|
||||
@@ -180,12 +181,13 @@ public class InboundPlanService {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets, null);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets, null, null);
|
||||
}
|
||||
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual) {
|
||||
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual,
|
||||
WarehouseResourceGateway.WarehouseReference receivingWarehouse) {
|
||||
String containerNo = requiredContainerNo(orders.get(0).containerNo());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
@@ -262,7 +264,7 @@ public class InboundPlanService {
|
||||
orderMapper.insertHistory(customerId, order.orderId(), order.currentStatus(),
|
||||
"Inbound order converted to inbound plan " + plan.getPlanNo(), now);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
return detail(customerId, plan, receivingWarehouse);
|
||||
}
|
||||
|
||||
/** 同一来源柜的每个尾程订单独立建计划;整个柜级批次共用一个事务。 */
|
||||
@@ -326,12 +328,14 @@ public class InboundPlanService {
|
||||
}
|
||||
wholePallets.add(sourcePallets);
|
||||
}
|
||||
requireSingleWarehouseId(snapshots.stream().flatMap(List::stream)
|
||||
.map(SourceOrderSnapshot::overseasWarehouseId).toList());
|
||||
|
||||
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));
|
||||
wholePallets.get(index), null, null));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
}
|
||||
@@ -623,16 +627,21 @@ public class InboundPlanService {
|
||||
}
|
||||
|
||||
private InboundPlanResponse detail(String customerId, InboundPlan plan) {
|
||||
return detail(customerId, plan, null);
|
||||
}
|
||||
|
||||
private InboundPlanResponse detail(String customerId, InboundPlan plan,
|
||||
WarehouseResourceGateway.WarehouseReference resolvedWarehouse) {
|
||||
if (customerId != null && !customerId.equals(plan.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
InboundPlanOrderMapper.ReceivingWarehouseSnapshot receivingWarehouse = orderMapper.findReceivingWarehouse(
|
||||
plan.getCustomerId(), plan.getSourceOrderId());
|
||||
if (receivingWarehouse == null || trimToNull(receivingWarehouse.overseasWarehouseId()) == null
|
||||
|| trimToNull(receivingWarehouse.overseasWarehouseCode()) == null
|
||||
|| trimToNull(receivingWarehouse.overseasWarehouseName()) == null) {
|
||||
if (receivingWarehouse == null || trimToNull(receivingWarehouse.overseasWarehouseId()) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "入库计划关联订单缺少接柜仓预分配事实");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference authoritativeWarehouse = resolvedWarehouse == null
|
||||
? requiredReceivingWarehouse(receivingWarehouse.overseasWarehouseId()) : resolvedWarehouse;
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId()).orderByAsc(InboundPlanLine::getId));
|
||||
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
@@ -642,7 +651,7 @@ public class InboundPlanService {
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanPalletAllocation::getId));
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
|
||||
plan.getContainerNo(), receivingWarehouse.overseasWarehouseCode(), receivingWarehouse.overseasWarehouseName(),
|
||||
plan.getContainerNo(), authoritativeWarehouse.warehouseCode(), authoritativeWarehouse.warehouseName(),
|
||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
|
||||
lines.stream().mapToInt(InboundPlanLine::getRequiredPackageCount).sum(),
|
||||
allocations.stream().mapToInt(InboundPlanPalletAllocation::getPickedPackageCount).sum(),
|
||||
@@ -663,6 +672,30 @@ public class InboundPlanService {
|
||||
Boolean.TRUE.equals(plan.getUrgentFlag()), plan.getCreationMode(), plan.getPlanRemark());
|
||||
}
|
||||
|
||||
private WarehouseResourceGateway.WarehouseReference requiredReceivingWarehouse(String warehouseId) {
|
||||
String stableId = trimToNull(warehouseId);
|
||||
if (stableId == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓未预分配");
|
||||
}
|
||||
try {
|
||||
return warehouseGateway.requireActiveSharedWarehouse(Long.parseLong(stableId));
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓ID格式无效", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private String requireSingleWarehouseId(List<String> warehouseIds) {
|
||||
List<String> normalized = warehouseIds.stream().map(InboundPlanService::trimToNull).toList();
|
||||
if (normalized.stream().anyMatch(Objects::isNull)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在未分配接柜仓的尾程订单");
|
||||
}
|
||||
java.util.Set<String> distinct = new java.util.HashSet<>(normalized);
|
||||
if (distinct.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单接柜仓不一致");
|
||||
}
|
||||
return distinct.iterator().next();
|
||||
}
|
||||
|
||||
private BigDecimal volume(SourceOrderSnapshot order) {
|
||||
return Objects.requireNonNullElse(order.volumeCbm(), BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@@ -1856,16 +1856,13 @@ components:
|
||||
items: { $ref: '#/components/schemas/LastlegOrderManualNode' }
|
||||
ManualInboundPlanRequest:
|
||||
type: object
|
||||
required: [customerId, orderId, expectedOrderVersion, plannedWarehouseId, plannedWarehouseCode, plannedWarehouseName, plannedArrivalDate, inboundCode, palletCount, palletType, assigned, mergeRequired, urgent]
|
||||
description: 单票人工计划只能使用所属柜已冻结的接柜仓;接柜仓变更必须通过柜级接口完成。
|
||||
required: [customerId, orderId, expectedOrderVersion, plannedArrivalDate, palletCount, palletType, assigned, mergeRequired, urgent]
|
||||
properties:
|
||||
customerId: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
orderId: { type: integer, format: int64, minimum: 1 }
|
||||
expectedOrderVersion: { type: integer, minimum: 0 }
|
||||
plannedWarehouseId: { type: integer, format: int64, minimum: 1 }
|
||||
plannedWarehouseCode: { type: string, minLength: 1, maxLength: 64 }
|
||||
plannedWarehouseName: { type: string, minLength: 1, maxLength: 255 }
|
||||
plannedArrivalDate: { type: string, format: date }
|
||||
inboundCode: { type: string, minLength: 1, maxLength: 128 }
|
||||
palletCount: { type: integer, minimum: 1, maximum: 999 }
|
||||
palletType: { type: string, minLength: 1, maxLength: 64 }
|
||||
assigned: { type: boolean }
|
||||
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
package com.baiyue.lastleg.inboundplan;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -16,6 +17,15 @@ class TailPlanCommandOpenApiContractTest {
|
||||
String api = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/inbound-plans/manual:"));
|
||||
assertFalse(api.contains("/api/platform/v1/last-leg/inbound-plans/convert:"));
|
||||
assertFalse(api.contains("plannedWarehouseId"));
|
||||
assertFalse(api.contains("plannedWarehouseCode"));
|
||||
assertFalse(api.contains("plannedWarehouseName"));
|
||||
int manualSchemaStart = api.indexOf(" ManualInboundPlanRequest:");
|
||||
int manualSchemaEnd = api.indexOf("\n PageMetadata:", manualSchemaStart);
|
||||
assertTrue(manualSchemaStart >= 0 && manualSchemaEnd > manualSchemaStart);
|
||||
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("inboundCode"));
|
||||
assertFalse(java.util.Arrays.stream(ManualInboundPlanRequest.class.getRecordComponents())
|
||||
.anyMatch(component -> "inboundCode".equals(component.getName())));
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/hold:"));
|
||||
assertFalse(api.contains("/api/platform/v1/last-leg/orders/{id}/detained:"));
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/manual-nodes:"));
|
||||
|
||||
+1
-2
@@ -101,7 +101,6 @@ class InboundPlanControllerTest {
|
||||
|
||||
private static ManualInboundPlanRequest manualRequest() {
|
||||
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", LocalDate.of(2026, 9, 8),
|
||||
"FBA-001", 2, "STANDARD", false, true, true, "加急入仓");
|
||||
LocalDate.of(2026, 9, 8), 2, "STANDARD", false, true, true, "加急入仓");
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -34,4 +34,17 @@ class InboundPlanOrderMapperContractTest {
|
||||
assertTrue(sql.contains("customer_id=#{customerId} AND id=#{orderId}"));
|
||||
assertTrue(!sql.contains("destination_warehouse"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanLocksEveryOrderInTheSelectedStableContainer() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("lockContainerWarehouseFacts", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("c.source_container_booking_id=target.source_container_booking_id"));
|
||||
assertTrue(sql.contains("target.customer_id=#{customerId} AND target.id=#{orderId}"));
|
||||
assertTrue(sql.contains("c.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("ORDER BY c.customer_id,c.id FOR UPDATE"));
|
||||
}
|
||||
}
|
||||
|
||||
+71
-4
@@ -78,6 +78,12 @@ class InboundPlanServiceTest {
|
||||
org.mockito.Mockito.lenient().when(orderMapper.findReceivingWarehouse(any(), any())).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
org.mockito.Mockito.lenient().when(orderMapper.lockContainerWarehouseFacts(any(), any())).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef(
|
||||
"100", 2L, "BOOKING-1", "193000000000200003")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,8 +139,7 @@ class InboundPlanServiceTest {
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any()))
|
||||
.thenReturn(1);
|
||||
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", LocalDate.of(2026, 9, 15),
|
||||
"RK-2", 2, "STANDARD", true, true, true, "优先收货");
|
||||
LocalDate.of(2026, 9, 15), 2, "STANDARD", true, true, true, "优先收货");
|
||||
|
||||
var result = service.convertManual(request, "manual-2", "900");
|
||||
|
||||
@@ -149,6 +154,46 @@ class InboundPlanServiceTest {
|
||||
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any());
|
||||
verify(commandMapper).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
verify(orderMapper, never()).lockContainerOrders(any());
|
||||
verify(orderMapper).lockContainerWarehouseFacts("100", 2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyCreatesPlanWhenStableWarehouseIdExistsWithoutDisplaySnapshots() {
|
||||
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200003", null, null, null,
|
||||
"WH-A", "WH-A", "仓A", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(3), eq(false), eq(0), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
|
||||
LocalDate.of(2026, 9, 15), 3, "STANDARD", true, false, false, null),
|
||||
"manual-id-only", "900");
|
||||
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
verify(warehouseGateway).requireActiveSharedWarehouse(193000000000200003L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanRejectsMixedContainerWarehousesBeforeLockingOrderOrWriting() {
|
||||
when(orderMapper.lockContainerWarehouseFacts("100", 2L)).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef("100", 2L, "BOOKING-1", "193000000000200003"),
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef("200", 3L, "BOOKING-1", "193000000000200004")));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
|
||||
1, "STANDARD", false, false, false, null), "mixed-warehouse", "900"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,8 +205,8 @@ class InboundPlanServiceTest {
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 5, 193000000000200003L, "US-LAX-01", "洛杉矶仓",
|
||||
LocalDate.of(2026, 9, 15), "RK-2", 1, "STANDARD", false, false, false, null),
|
||||
new ManualInboundPlanRequest("100", 2L, 5, LocalDate.of(2026, 9, 15),
|
||||
1, "STANDARD", false, false, false, null),
|
||||
"manual-held", "900"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
@@ -217,6 +262,8 @@ class InboundPlanServiceTest {
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot("88", "US-NY-01", "纽约仓"));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(88L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(88L, "US-NY-01", "纽约仓", "ACTIVE"));
|
||||
|
||||
var result = service.detailByPlanNo("100", "LIP-1");
|
||||
|
||||
@@ -440,6 +487,26 @@ class InboundPlanServiceTest {
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerConversionRejectsMixedWarehouseSnapshotsBeforeCreatingAnyPlan() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "200", "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("200", 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
|
||||
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
|
||||
var refs = List.of(
|
||||
|
||||
Reference in New Issue
Block a user