允许私人地址按独立托盘生成海外入库计划
This commit is contained in:
+2
-2
@@ -44,7 +44,7 @@ public interface InboundPlanOrderMapper {
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),(SELECT MIN(NULLIF(d.destination_warehouse_code,'')) FROM ll_order_detail d WHERE d.order_id=o.id)) AS destinationWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_name,''),(SELECT MIN(NULLIF(d.destination_warehouse_name,'')) FROM ll_order_detail d WHERE d.order_id=o.id)) AS destinationWarehouseName," +
|
||||
"CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 ELSE " +
|
||||
"(SELECT COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) FROM ll_order_detail d WHERE d.order_id=o.id) END AS destinationCount," +
|
||||
"(SELECT COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),NULLIF(TRIM(d.destination_warehouse_name),''),'#EMPTY#')) FROM ll_order_detail d WHERE d.order_id=o.id) END AS destinationCount," +
|
||||
"o.detained AS detained,o.version AS version,p.plan_no AS planNo,p.status AS planStatus," +
|
||||
"p.pallet_count AS plannedPalletCount," +
|
||||
"(SELECT COUNT(*) FROM ll_inbound_plan_pallet pp WHERE pp.customer_id=p.customer_id " +
|
||||
@@ -93,7 +93,7 @@ public interface InboundPlanOrderMapper {
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_name,''),MIN(NULLIF(d.destination_warehouse_name,''))) AS destinationWarehouseName," +
|
||||
"CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 " +
|
||||
"ELSE COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) END AS destinationCount," +
|
||||
"ELSE COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),NULLIF(TRIM(d.destination_warehouse_name),''),'#EMPTY#')) END AS destinationCount," +
|
||||
"0 AS destinationMismatchCount,o.detained AS detained,o.version AS version " +
|
||||
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " +
|
||||
|
||||
+9
-2
@@ -291,7 +291,8 @@ public class InboundPlanService {
|
||||
boolean fba = isFbaDestination(row);
|
||||
if (fba ? trimToNull(row.fbaWarehouseCode()) == null
|
||||
: row.destinationCount() != 1 || trimToNull(row.destinationWarehouseId()) == null
|
||||
&& trimToNull(row.destinationWarehouseCode()) == null) blockers.add("DESTINATION_NOT_READY");
|
||||
&& trimToNull(row.destinationWarehouseCode()) == null
|
||||
&& trimToNull(row.destinationWarehouseName()) == null) blockers.add("DESTINATION_NOT_READY");
|
||||
if (row.packageCount() < 0 || row.volumeCbm() == null || row.volumeCbm().signum() < 0
|
||||
|| row.packageCount() > 0 && row.volumeCbm().signum() == 0) blockers.add("CARGO_FACT_INVALID");
|
||||
}
|
||||
@@ -1181,7 +1182,13 @@ public class InboundPlanService {
|
||||
private String warehouseKey(SourceOrderSnapshot order) {
|
||||
String id = trimToNull(order.destinationWarehouseId());
|
||||
String code = trimToNull(order.destinationWarehouseCode());
|
||||
return id != null ? "ID:" + id : code == null ? null : "CODE:" + code;
|
||||
if (id != null) return "ID:" + id;
|
||||
if (code != null) return "CODE:" + code;
|
||||
if (trimToNull(order.fbaWarehouseCode()) == null
|
||||
&& trimToNull(order.destinationWarehouseName()) != null) {
|
||||
return "PRIVATE:" + order.orderId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
|
||||
@@ -773,7 +773,7 @@ paths:
|
||||
/api/platform/v1/last-leg/inbound-plans/container-preview:
|
||||
get:
|
||||
summary: 预览按柜生成入库计划
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;按稳定 sourceContainerBookingId 返回不受订单分页影响的全柜事实。分组固定为 FBA、一件代发、私人地址,FBA 按目的仓编码/名称/ID 稳定排序;托数及 readiness/blockers 只由服务端计算。确认时必须原样回传 snapshotVersion 与 snapshotHash,服务端仍会锁柜重算。
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;按稳定 sourceContainerBookingId 返回不受订单分页影响的全柜事实。分组固定为 FBA、一件代发、私人地址,FBA 按目的仓编码/名称/ID 稳定排序;私人地址允许目的仓 ID 和编码为空,仍须具备票内一致的非空地址名称和有效接柜仓绑定。托数及 readiness/blockers 只由服务端计算。确认时必须原样回传 snapshotVersion 与 snapshotHash,服务端仍会锁柜重算。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sourceContainerBookingId, in: query, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
|
||||
|
||||
+10
@@ -48,6 +48,15 @@ class InboundPlanOrderMapperContractTest {
|
||||
row = jdbc.queryForMap(sql, "C1", 1L);
|
||||
assertEquals("ABE3", row.get("fbaWarehouseCode"));
|
||||
assertEquals(row.get("destinationWarehouseCode"), row.get("fbaWarehouseCode"));
|
||||
|
||||
jdbc.update("INSERT INTO ll_order (id,customer_id) VALUES (2,'C1')");
|
||||
jdbc.update("INSERT INTO ll_order_detail VALUES (2,NULL,NULL,'私人地址 A')");
|
||||
row = jdbc.queryForMap(sql, "C1", 2L);
|
||||
assertEquals(1L, ((Number) row.get("destinationCount")).longValue());
|
||||
assertEquals("私人地址 A", row.get("destinationWarehouseName"));
|
||||
jdbc.update("INSERT INTO ll_order_detail VALUES (2,NULL,NULL,'私人地址 B')");
|
||||
row = jdbc.queryForMap(sql, "C1", 2L);
|
||||
assertEquals(2L, ((Number) row.get("destinationCount")).longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,6 +66,7 @@ class InboundPlanOrderMapperContractTest {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),NULLIF(o.fba_warehouse_code,'')) AS fbaWarehouseCode"),
|
||||
method.getName());
|
||||
assertTrue(sql.contains("NULLIF(TRIM(d.destination_warehouse_name),'')"), method.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
@@ -70,6 +70,33 @@ class ContainerInboundPlanPreviewTest {
|
||||
assertEquals(List.of("DESTINATION_NOT_READY"), result.groups().get(4).orders().get(0).blockers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void privateAddressReadinessRequiresOneNamedDestinationWithoutMasterWarehouseIdentity() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-PRIVATE")).thenReturn(List.of(
|
||||
privateAddressRow(1L, "私人地址 A", 1),
|
||||
privateAddressRow(2L, " ", 1),
|
||||
privateAddressRow(3L, "私人地址 B", 2)));
|
||||
|
||||
var result = service.previewContainer("BOOKING-PRIVATE");
|
||||
var orders = result.groups().stream().flatMap(group -> group.orders().stream()).toList();
|
||||
var ready = orders.stream().filter(order -> order.orderId().equals("1")).findFirst().orElseThrow();
|
||||
assertEquals("READY", ready.readiness());
|
||||
assertEquals("PRIVATE_ADDRESS", ready.destinationType());
|
||||
assertEquals(List.of(), ready.blockers());
|
||||
assertEquals(1, ready.targetPalletCount());
|
||||
assertEquals(2, result.blockedOrderCount());
|
||||
orders.stream().filter(order -> !order.orderId().equals("1")).forEach(order ->
|
||||
assertEquals(List.of("DESTINATION_NOT_READY"), order.blockers()));
|
||||
}
|
||||
|
||||
private InboundPlanOrderMapper.ContainerPreviewOrderRow privateAddressRow(long id, String name, int destinations) {
|
||||
return new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", id, "LM-" + id, "FO-" + id,
|
||||
"测试客户", "FIRST_LEG", "OVERSEAS", "IN-" + id, "PENDING_ASSIGNMENT", null,
|
||||
"MSCU6639870", 1, 4, BigDecimal.ONE, "193000000000200003", "US-LAX-01",
|
||||
"洛杉矶仓", null, null, null, name, destinations, false, 1,
|
||||
null, null, null, null, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashesBlockedPreviewEvenWhenContainerAndWarehouseFactsAreNull() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-NULL")).thenReturn(List.of(
|
||||
|
||||
+38
@@ -37,6 +37,7 @@ import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
@@ -101,6 +102,43 @@ class InboundPlanServiceTest {
|
||||
.thenReturn(java.time.LocalDate.of(2026, 9, 15));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsPrivateAddressWithoutCreatingADestinationWarehouseIdentity() {
|
||||
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-PRIVATE",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 4, 1,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null, null, null, "私人地址 A", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convert("100", 2L);
|
||||
|
||||
assertEquals(1, result.palletCount());
|
||||
ArgumentCaptor<InboundPlanLine> line = ArgumentCaptor.forClass(InboundPlanLine.class);
|
||||
verify(lineMapper).insert(line.capture());
|
||||
assertNull(line.getValue().getDestinationWarehouseId());
|
||||
assertNull(line.getValue().getDestinationWarehouseCode());
|
||||
assertEquals("私人地址 A", line.getValue().getDestinationWarehouseName());
|
||||
assertEquals(4, line.getValue().getRequiredPackageCount());
|
||||
ArgumentCaptor<InboundPlanPallet> pallet = ArgumentCaptor.forClass(InboundPlanPallet.class);
|
||||
verify(palletMapper).insert(pallet.capture());
|
||||
assertEquals("私人地址 A", pallet.getValue().getDestinationWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPrivateAddressWithoutANamedDestination() {
|
||||
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-PRIVATE",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 4, 1,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null, null, null, " ", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
|
||||
ResponseStatusException failure = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convert("100", 2L));
|
||||
|
||||
assertEquals(400, failure.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsOnlyTheSelectedInboundOrderToItsOwnPlan() throws Exception {
|
||||
assertNotNull(InboundPlanService.class.getMethod("convert", String.class, Long.class)
|
||||
|
||||
Reference in New Issue
Block a user