fix: materialize signed manual pallet plans

This commit is contained in:
2026-09-10 16:33:32 +08:00
parent f5d6c37080
commit 1ce99e710d
10 changed files with 47 additions and 14 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
| 功能 | 入口 | | 功能 | 入口 |
|---|---| |---|---|
| 内部模块化基线与依赖分析 | `docs/lastleg-internal-modularization-baseline.md`、`scripts/analyze-lastleg-dependencies.mjs`、`scripts/analyze-lastleg-dependencies.test.mjs`、`src/test/resources/architecture/lastleg-slice-dependencies.json`;只约束本服务内部包依赖,不改变部署单元或数据库边界 | | 内部模块化基线与依赖分析 | `docs/lastleg-internal-modularization-baseline.md`、`scripts/analyze-lastleg-dependencies.mjs`、`scripts/analyze-lastleg-dependencies.test.mjs`、`src/test/resources/architecture/lastleg-slice-dependencies.json`;只约束本服务内部包依赖,不改变部署单元或数据库边界 |
| 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码和有效计划并分页返回全部稳定来源柜,已完成计划转换的柜仍保留用于接柜仓查询与修改,生成资格只由 `allPlansReady/pendingPlanCount` 表达;`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 | | 事件接收、平台订单列表/详情/导出、柜级进度、目的仓调整 | `src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java`;`/api/platform/v1/last-leg/orders/container-summaries` 按 `sourceContainerBookingId` 服务端聚合全柜订单、入仓码、有效计划及待补真实托盘计划并分页返回全部稳定来源柜,已完成计划转换的柜仍保留用于接柜仓查询与修改,生成资格由 `pendingPlanCount/resourcePendingPlanCount` 表达;`/{sourceContainerBookingId}` 返回柜内全部订单/入仓码/计划状态且不受订单分页截断;`plannedArrivalAt` 只取装柜完成事件 `estimatedWarehouseArrivalAt` 冻结到 `ll_order.estimated_warehouse_at` 的正式事实,按其升序、null 最后、稳定柜 ID 排序;目的仓调整仅接受国内 Warehouse Service 平台公共 `ACTIVE + FBA` 主档,写入前由 `service/FbaWarehouseValidator.java` 经内部只读目录校验 ID/编码/名称一致 |
| 尾程订单完整计划、暂扣与人工节点 | `controller/LastlegOrderController.java`、`service/LastlegOrderCommandService.java`、`inboundplan/service/InboundPlanService.java`、`dto/LastlegOrderCommandDtos.java`;单票计划写入校验订单版本、客户范围、订单冻结入仓码、稳定接柜仓 ID、当前仓库主档与幂等键;接柜仓编码/名称以该 ID 当前目录快照为准,不要求订单残留旧展示快照。暂扣/解除写不可变历史并发布 `InboundOrderDetainedOutboxService`,已拣货、已发运或已有出库单时禁止新增暂扣;暂扣期间不得进入排车或出库。人工节点必须一次提交固定八行完整快照且至少一个时间,首次保存后永久使用 `MANUAL` 来源,不得回退柜级节点或反向修改头程船节点 | | 尾程订单完整计划、暂扣与人工节点 | `controller/LastlegOrderController.java`、`service/LastlegOrderCommandService.java`、`inboundplan/service/InboundPlanService.java`、`dto/LastlegOrderCommandDtos.java`;单票计划写入校验订单版本、客户范围、订单冻结入仓码、稳定接柜仓 ID、当前仓库主档与幂等键;接柜仓编码/名称以该 ID 当前目录快照为准,不要求订单残留旧展示快照。暂扣/解除写不可变历史并发布 `InboundOrderDetainedOutboxService`,已拣货、已发运或已有出库单时禁止新增暂扣;暂扣期间不得进入排车或出库。人工节点必须一次提交固定八行完整快照且至少一个时间,首次保存后永久使用 `MANUAL` 来源,不得回退柜级节点或反向修改头程船节点 |
| 客户尾程订单列表与详情 | `controller/MerchantLastlegOrderController.java`;客户范围只取认证身份,稳定类型/状态见 `model/LastlegOrderType.java`、`LastlegOrderStatus.java` | | 客户尾程订单列表与详情 | `controller/MerchantLastlegOrderController.java`;客户范围只取认证身份,稳定类型/状态见 `model/LastlegOrderType.java`、`LastlegOrderStatus.java` |
| 装柜完成事件消费 | `service/LoadingCompletedConsumer.java`;当前生产信封为 `FIRST_LEG_LOADING_PLAN_COMPLETED` V2,消费端冻结 `destinationType`:FBA 可按箱数且不生成逐箱作业账,OVERSEAS 必须逐箱校验并生成箱级事实;历史缺字段事件仅按明确 FBA 单号或目的仓与接柜仓一致性安全推断,歧义直接拒绝 | | 装柜完成事件消费 | `service/LoadingCompletedConsumer.java`;当前生产信封为 `FIRST_LEG_LOADING_PLAN_COMPLETED` V2,消费端冻结 `destinationType`:FBA 可按箱数且不生成逐箱作业账,OVERSEAS 必须逐箱校验并生成箱级事实;历史缺字段事件仅按明确 FBA 单号或目的仓与接柜仓一致性安全推断,歧义直接拒绝 |
@@ -18,6 +18,7 @@ public record ContainerPlanProgressSummary(
long planCount, long planCount,
long plannedInboundCodeCount, long plannedInboundCodeCount,
long pendingPlanCount, long pendingPlanCount,
long resourcePendingPlanCount,
LocalDateTime plannedArrivalAt, LocalDateTime plannedArrivalAt,
boolean allPlansReady, boolean allPlansReady,
String blockingReason String blockingReason
@@ -30,6 +31,6 @@ public record ContainerPlanProgressSummary(
String blockingReason) { String blockingReason) {
this(sourceContainerBookingId, containerNo, null, null, null, -1, false, null, this(sourceContainerBookingId, containerNo, null, null, null, -1, false, null,
totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount,
plannedInboundCodeCount, pendingPlanCount, plannedArrivalAt, allPlansReady, blockingReason); plannedInboundCodeCount, pendingPlanCount, 0, plannedArrivalAt, allPlansReady, blockingReason);
} }
} }
@@ -261,7 +261,8 @@ public class InboundPlanService {
&& row.materializedPalletCount() > 0) { && row.materializedPalletCount() > 0) {
blockers.add("PLAN_PALLET_BINDING_INCOMPLETE"); blockers.add("PLAN_PALLET_BINDING_INCOMPLETE");
} }
if (planned && !materialized && !"PENDING_ASSIGNMENT".equals(row.planStatus())) { if (planned && !materialized && !("PENDING_ASSIGNMENT".equals(row.planStatus())
|| "SIGNED".equals(row.planStatus()))) {
blockers.add("PLAN_ALREADY_STARTED"); blockers.add("PLAN_ALREADY_STARTED");
} }
String readiness = planned ? materialized ? "PLANNED" String readiness = planned ? materialized ? "PLANNED"
@@ -636,8 +637,8 @@ public class InboundPlanService {
|| !Objects.equals(ref.orderId(), prepared.orderId())) { || !Objects.equals(ref.orderId(), prepared.orderId())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "人工计划与柜级托盘预留快照不一致"); throw new ResponseStatusException(HttpStatus.CONFLICT, "人工计划与柜级托盘预留快照不一致");
} }
if (!"PENDING_ASSIGNMENT".equals(plan.getStatus())) { if (!("PENDING_ASSIGNMENT".equals(plan.getStatus()) || "SIGNED".equals(plan.getStatus()))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "仅未开始作业的已有计划可由整柜生成补齐托盘"); throw new ResponseStatusException(HttpStatus.CONFLICT, "仅待入库或已签收但未实收的已有计划可由整柜生成补齐托盘");
} }
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>() List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
.eq(InboundPlanPallet::getCustomerId, plan.getCustomerId()) .eq(InboundPlanPallet::getCustomerId, plan.getCustomerId())
@@ -42,6 +42,10 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
"AND NULLIF(TRIM(p.status),'') IS NOT NULL THEN NULLIF(TRIM(o.inbound_code),'') END) AS plannedInboundCodeCount," + "AND NULLIF(TRIM(p.status),'') IS NOT NULL THEN NULLIF(TRIM(o.inbound_code),'') END) AS plannedInboundCodeCount," +
"SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL OR NULLIF(TRIM(p.status),'') IS NULL " + "SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL OR NULLIF(TRIM(p.status),'') IS NULL " +
"THEN 1 ELSE 0 END) AS pendingPlanCount," + "THEN 1 ELSE 0 END) AS pendingPlanCount," +
"SUM(CASE WHEN p.id IS NOT NULL AND COALESCE(p.pallet_count,0)&gt;0 AND " +
"(SELECT COUNT(*) FROM ll_inbound_plan_pallet pp WHERE pp.customer_id=p.customer_id " +
"AND pp.plan_id=p.id AND pp.warehouse_pallet_id IS NOT NULL)&lt;COALESCE(p.pallet_count,0) " +
"THEN 1 ELSE 0 END) AS resourcePendingPlanCount," +
"CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) " + "CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) " +
"AND COUNT(DISTINCT o.estimated_warehouse_at)=1 THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS plannedArrivalAt," + "AND COUNT(DISTINCT o.estimated_warehouse_at)=1 THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS plannedArrivalAt," +
"CASE WHEN SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)=0 " + "CASE WHEN SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)=0 " +
@@ -176,7 +176,7 @@ public class LastlegOrderService {
summary.assignmentVersion(), invalidContainer ? false : summary.canChangeReceivingWarehouse(), summary.assignmentVersion(), invalidContainer ? false : summary.canChangeReceivingWarehouse(),
invalidContainer ? issue : summary.receivingWarehouseLockReason(), summary.totalOrderCount(), invalidContainer ? issue : summary.receivingWarehouseLockReason(), summary.totalOrderCount(),
summary.inboundCodeCount(), summary.missingInboundCodeCount(), summary.planCount(), summary.inboundCodeCount(), summary.missingInboundCodeCount(), summary.planCount(),
summary.plannedInboundCodeCount(), summary.pendingPlanCount(), summary.plannedArrivalAt(), summary.plannedInboundCodeCount(), summary.pendingPlanCount(), summary.resourcePendingPlanCount(), summary.plannedArrivalAt(),
false, issue); false, issue);
} }
@@ -1289,7 +1289,7 @@ components:
items: { type: string, pattern: '^[1-9][0-9]*$' } items: { type: string, pattern: '^[1-9][0-9]*$' }
ContainerPlanProgressSummary: ContainerPlanProgressSummary:
type: object type: object
required: [sourceContainerBookingId, containerNo, overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName, assignmentVersion, canChangeReceivingWarehouse, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, allPlansReady] required: [sourceContainerBookingId, containerNo, overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName, assignmentVersion, canChangeReceivingWarehouse, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, resourcePendingPlanCount, allPlansReady]
properties: properties:
sourceContainerBookingId: { type: string } sourceContainerBookingId: { type: string }
containerNo: { type: string, pattern: '^[A-Z]{3}[UJZ][0-9]{7}$', description: 装柜完成事件非空且通过 ISO 6346 校验位验证的规范化真实集装箱号;不使用 cabinetNo、订单号或内部 ID 回退 } containerNo: { type: string, pattern: '^[A-Z]{3}[UJZ][0-9]{7}$', description: 装柜完成事件非空且通过 ISO 6346 校验位验证的规范化真实集装箱号;不使用 cabinetNo、订单号或内部 ID 回退 }
@@ -1305,6 +1305,7 @@ components:
planCount: { type: integer, format: int64 } planCount: { type: integer, format: int64 }
plannedInboundCodeCount: { type: integer, format: int64 } plannedInboundCodeCount: { type: integer, format: int64 }
pendingPlanCount: { type: integer, format: int64, description: 尚未生成有效计划的尾程订单数,即未操作数 } pendingPlanCount: { type: integer, format: int64, description: 尚未生成有效计划的尾程订单数,即未操作数 }
resourcePendingPlanCount: { type: integer, format: int64, description: 已有有效计划但尚未完整绑定海外仓真实托盘的订单数 }
plannedArrivalAt: { type: string, format: date-time, nullable: true, description: 仅来源于装柜完成事件 estimatedWarehouseArrivalAt 冻结的 ll_order.estimated_warehouse_at } plannedArrivalAt: { type: string, format: date-time, nullable: true, description: 仅来源于装柜完成事件 estimatedWarehouseArrivalAt 冻结的 ll_order.estimated_warehouse_at }
allPlansReady: { type: boolean, description: 全柜每票均有入仓码且均存在有效独立计划时为 true } allPlansReady: { type: boolean, description: 全柜每票均有入仓码且均存在有效独立计划时为 true }
blockingReason: { type: string, nullable: true } blockingReason: { type: string, nullable: true }
@@ -103,9 +103,33 @@ class ContainerInboundPlanPreviewTest {
.map(ContainerInboundPlanPreviewResponse.Group::destinationType).toList()); .map(ContainerInboundPlanPreviewResponse.Group::destinationType).toList());
} }
@Test
void signedManualPlanWithoutWarehousePalletsRemainsResourcePending() {
when(orderMapper.containerPreviewOrders("BOOKING-SIGNED")).thenReturn(List.of(
row(1L, "LM-SIGNED", "FIRST_LEG", "FBA", "ACY2", "Amazon ACY2", 3,
"IP-SIGNED", 3, 1, "SIGNED", 0)
));
var result = service.previewContainer("BOOKING-SIGNED");
assertEquals(true, result.ready());
assertEquals(1, result.pendingOrderCount());
assertEquals(3, result.pendingPalletCount());
assertEquals("RESOURCE_PENDING", result.groups().get(0).orders().get(0).readiness());
}
private InboundPlanOrderMapper.ContainerPreviewOrderRow row( private InboundPlanOrderMapper.ContainerPreviewOrderRow row(
long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName, long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName,
int recommendedPallets, String planNo, Integer plannedPallets, int version) { int recommendedPallets, String planNo, Integer plannedPallets, int version) {
return row(id, orderNo, orderType, destinationType, destinationCode, destinationName,
recommendedPallets, planNo, plannedPallets, version,
planNo == null ? null : "PENDING_ASSIGNMENT", plannedPallets);
}
private InboundPlanOrderMapper.ContainerPreviewOrderRow row(
long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName,
int recommendedPallets, String planNo, Integer plannedPallets, int version, String planStatus,
Integer materializedPallets) {
return new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", id, orderNo, "FO-" + id, return new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", id, orderNo, "FO-" + id,
"测试客户", orderType, destinationType, "IN-" + id, "PENDING_ASSIGNMENT", planNo, "测试客户", orderType, destinationType, "IN-" + id, "PENDING_ASSIGNMENT", planNo,
"MSCU6639870", recommendedPallets, 10, new BigDecimal("1.0000"), "MSCU6639870", recommendedPallets, 10, new BigDecimal("1.0000"),
@@ -113,6 +137,6 @@ class ContainerInboundPlanPreviewTest {
"FBA".equals(destinationType) || destinationCode == null ? null : "WH-" + id, "FBA".equals(destinationType) || destinationCode == null ? null : "WH-" + id,
"FBA".equals(destinationType) ? null : destinationCode, destinationName, "FBA".equals(destinationType) ? null : destinationCode, destinationName,
"FBA".equals(destinationType) || destinationCode == null ? 0 : 1, false, version, planNo, "FBA".equals(destinationType) || destinationCode == null ? 0 : 1, false, version, planNo,
planNo == null ? null : "PENDING_ASSIGNMENT", plannedPallets, plannedPallets); planStatus, plannedPallets, materializedPallets);
} }
} }
@@ -181,10 +181,11 @@ class InboundPlanServiceTest {
} }
@Test @Test
void containerConversionBindsRealWarehousePalletsBackToExistingManualPlan() { void containerConversionBindsRealWarehousePalletsBackToSignedManualPlanBeforeReceiving() {
InboundPlan plan = existingPlan("100", 2L, "IP-MANUAL"); InboundPlan plan = existingPlan("100", 2L, "IP-MANUAL");
plan.setCreationMode("MANUAL"); plan.setCreationMode("MANUAL");
plan.setPalletCount(2); plan.setPalletCount(2);
plan.setStatus("SIGNED");
InboundPlanPallet first = new InboundPlanPallet(); InboundPlanPallet first = new InboundPlanPallet();
first.setId(301L); first.setCustomerId("100"); first.setPlanId(plan.getId()); first.setSequenceNo(1); first.setId(301L); first.setCustomerId("100"); first.setPlanId(plan.getId()); first.setSequenceNo(1);
InboundPlanPallet second = new InboundPlanPallet(); InboundPlanPallet second = new InboundPlanPallet();
@@ -27,7 +27,7 @@ class ContainerPlanProgressContractTest {
assertEquals(List.of("sourceContainerBookingId", "containerNo", "overseasWarehouseId", assertEquals(List.of("sourceContainerBookingId", "containerNo", "overseasWarehouseId",
"overseasWarehouseCode", "overseasWarehouseName", "assignmentVersion", "overseasWarehouseCode", "overseasWarehouseName", "assignmentVersion",
"canChangeReceivingWarehouse", "receivingWarehouseLockReason", "totalOrderCount", "inboundCodeCount", "canChangeReceivingWarehouse", "receivingWarehouseLockReason", "totalOrderCount", "inboundCodeCount",
"missingInboundCodeCount", "planCount", "plannedInboundCodeCount", "pendingPlanCount", "missingInboundCodeCount", "planCount", "plannedInboundCodeCount", "pendingPlanCount", "resourcePendingPlanCount",
"plannedArrivalAt", "allPlansReady", "blockingReason"), "plannedArrivalAt", "allPlansReady", "blockingReason"),
Arrays.stream(ContainerPlanProgressSummary.class.getRecordComponents()) Arrays.stream(ContainerPlanProgressSummary.class.getRecordComponents())
.map(value -> value.getName()).toList()); .map(value -> value.getName()).toList());
@@ -40,6 +40,7 @@ class ContainerPlanProgressContractTest {
LastlegOrderRequests.ContainerProgressQuery.class, long.class, long.class).getAnnotation(Select.class); LastlegOrderRequests.ContainerProgressQuery.class, long.class, long.class).getAnnotation(Select.class);
String sql = String.join("", rows.value()).toLowerCase(); String sql = String.join("", rows.value()).toLowerCase();
assertTrue(sql.contains("left join ll_inbound_plan")); assertTrue(sql.contains("left join ll_inbound_plan"));
assertTrue(sql.contains("ll_inbound_plan_pallet"));
assertTrue(sql.contains("o.estimated_warehouse_at")); assertTrue(sql.contains("o.estimated_warehouse_at"));
assertTrue(sql.contains("group by o.source_container_booking_id")); assertTrue(sql.contains("group by o.source_container_booking_id"));
assertFalse(sql.contains("having ")); assertFalse(sql.contains("having "));
@@ -97,7 +97,7 @@ class LastlegOrderServiceTest {
} }
var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900", var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900",
"US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 2, 2, 0, "US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 2, 2, 0,
null, true, null); 0, null, true, null);
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second)); when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second));
when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn( when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn(
new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE")); new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE"));
@@ -382,7 +382,7 @@ class LastlegOrderServiceTest {
var healthy = summary(false, 3, "存在3票尚未生成有效入库计划"); var healthy = summary(false, 3, "存在3票尚未生成有效入库计划");
var missingWarehouse = new ContainerPlanProgressSummary("BOOKING-2", "MSCU6639870", var missingWarehouse = new ContainerPlanProgressSummary("BOOKING-2", "MSCU6639870",
null, null, null, 0, true, null, 2, 2, 0, 0, 0, 2, null, null, null, 0, true, null, 2, 2, 0, 0, 0, 2,
LocalDateTime.of(2026, 10, 4, 9, 0), false, "存在2票尚未生成有效入库计划"); 0, LocalDateTime.of(2026, 10, 4, 9, 0), false, "存在2票尚未生成有效入库计划");
when(orders.countContainerProgress(query)).thenReturn(2L); when(orders.countContainerProgress(query)).thenReturn(2L);
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(healthy, missingWarehouse)); when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(healthy, missingWarehouse));
@@ -402,7 +402,7 @@ class LastlegOrderServiceTest {
var locked = new ContainerPlanProgressSummary("BOOKING-2", "TSTU0000061", var locked = new ContainerPlanProgressSummary("BOOKING-2", "TSTU0000061",
"193000000000200003", "US-LAX-01", "洛杉矶仓", 4, false, "193000000000200003", "US-LAX-01", "洛杉矶仓", 4, false,
"海外PDA已建立接柜会话,接柜仓已永久锁定", 1, 1, 0, 1, 1, 0, "海外PDA已建立接柜会话,接柜仓已永久锁定", 1, 1, 0, 1, 1, 0,
LocalDateTime.of(2026, 10, 4, 9, 0), true, null); 0, LocalDateTime.of(2026, 10, 4, 9, 0), true, null);
when(orders.countContainerProgress(query)).thenReturn(2L); when(orders.countContainerProgress(query)).thenReturn(2L);
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(ready, locked)); when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(ready, locked));
@@ -419,7 +419,7 @@ class LastlegOrderServiceTest {
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) { private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003", return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003",
"US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0, "US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0,
15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason); 15 - pending, 15 - pending, pending, 0, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason);
} }
@Test @Test