fix(lastleg): enforce container warehouse plan facts

This commit is contained in:
2026-09-08 11:00:39 +08:00
parent 61ffff1be0
commit cc6df5993f
8 changed files with 161 additions and 35 deletions
@@ -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:"));
@@ -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, "加急入仓");
}
}
@@ -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"));
}
}
@@ -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(