优化
This commit is contained in:
+2
-1
@@ -179,7 +179,8 @@ public class LastlegOrderController {
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/hold")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "暂扣或解除单票尾程订单",
|
||||
description = "要求版本与幂等键;暂扣只作用于该票并阻断其出库推进。Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
description = "要求版本与幂等键;新增暂扣只允许完成入库且尚未进入出库流程的订单,"
|
||||
+ "暂扣只作用于该票并仅阻断排车和出库。Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
public ApiResponse<LastlegOrderCommandDtos.HoldResponse> hold(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
|
||||
@@ -114,12 +114,6 @@ public final class LastlegOrderRequests {
|
||||
) {
|
||||
}
|
||||
|
||||
public record SetDetained(
|
||||
@NotNull Boolean detained,
|
||||
@NotBlank @Size(max = 500) String remark
|
||||
) {
|
||||
}
|
||||
|
||||
public record SetPalletMergeRequired(
|
||||
@NotNull Boolean palletMergeRequired
|
||||
) {
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ public interface InboundPlanOrderMapper {
|
||||
@Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," +
|
||||
"pallet_merge_flag=CASE WHEN #{mergeRequired}=1 THEN 'MERGE_REQUIRED' ELSE NULL END," +
|
||||
"pallet_count=#{palletCount},version=version+1,updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{orderId} AND version=#{expectedVersion} AND detained=0 " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{orderId} AND version=#{expectedVersion} " +
|
||||
"AND current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') AND inbound_order_no IS NULL")
|
||||
int markPlannedManual(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("planNo") String planNo, @Param("palletCount") int palletCount,
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.pda.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
@@ -23,7 +24,8 @@ public final class ReceivingContainerRequests {
|
||||
public record PalletResolve(@NotBlank String palletCode, @NotBlank String inboundCode) { }
|
||||
/** packageCount 是该托当前累计实收箱数,不是本次增量。 */
|
||||
public record BulkReceive(@NotBlank String inboundCode, @NotBlank String palletCode,
|
||||
@Positive int packageCount, Integer expectedPalletVersion,
|
||||
@Positive @Max(value = 66, message = "一个托盘最多只能收66箱,请拆分到其他托盘") int packageCount,
|
||||
Integer expectedPalletVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {
|
||||
public BulkReceive(String inboundCode, String palletCode, int packageCount, String idempotencyKey) {
|
||||
this(inboundCode, palletCode, packageCount, null, idempotencyKey);
|
||||
|
||||
+14
-1
@@ -73,6 +73,9 @@ public class ReceivingContainerService {
|
||||
"/api/pda/v1/last-leg/receiving-containers/current/domestic-loading-photos/{type}/content";
|
||||
private static final String PALLET_MERGE_HINT =
|
||||
"该入库计划可选择合托:合托时扫描同目的仓已使用的托盘码,独立收货时扫描新托盘码;不强制合托";
|
||||
private static final int MAX_PACKAGES_PER_PALLET = 66;
|
||||
private static final String PALLET_CAPACITY_MESSAGE =
|
||||
"一个托盘最多只能收66箱,请拆分到其他托盘";
|
||||
|
||||
@Resource private InboundPlanMapper plans;
|
||||
@Resource private InboundPlanLineMapper lines;
|
||||
@@ -364,6 +367,7 @@ public class ReceivingContainerService {
|
||||
ReceivingContainerRequests.BulkReceive request) {
|
||||
ReceivingContainerOperation replay = replay(operatorKey, "BULK_RECEIVE", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, ReceivingContainerResponses.Receipt.class);
|
||||
requirePalletCapacity(request.packageCount());
|
||||
ReceivingContainerSession session = requireSigned(operatorKey);
|
||||
InboundRef ref = requireInbound(session.getContainerNo(), request.inboundCode());
|
||||
if ("ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) {
|
||||
@@ -463,6 +467,10 @@ public class ReceivingContainerService {
|
||||
PalletSelection selection = resolveReceivingPallet(ref, request.palletCode(), true);
|
||||
WarehouseResourceGateway.WarehousePalletReference resource = selection.resource();
|
||||
InboundPlanPallet pallet = selection.pallet();
|
||||
long packagesOnPallet = cartons.selectCount(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getWarehousePalletId, resource.palletId())
|
||||
.eq(InboundPlanCarton::getStatus, "ON_PALLET"));
|
||||
requirePalletCapacity(packagesOnPallet + 1);
|
||||
workflow.validateCartonResolved(customerId, ref.plan(), ref.line(), pallet, request.cartonMark());
|
||||
Long resourceLockId = workflow.assignResolvedPallet(customerId, ref.plan(), pallet, resource);
|
||||
try {
|
||||
@@ -492,6 +500,8 @@ public class ReceivingContainerService {
|
||||
if (sourcePalletNo == null) throw notFound("DP or carton is not part of the current container");
|
||||
requireCompleteManifest(session.getContainerNo(), sourcePalletNo);
|
||||
List<DropshipPalletRef> matches = dropshipPalletRefs(session.getContainerNo(), sourcePalletNo);
|
||||
int total = matches.stream().mapToInt(value -> value(value.allocation().getPlannedPackageCount())).sum();
|
||||
requirePalletCapacity(total);
|
||||
boolean alreadyReceived = matches.stream().allMatch(match ->
|
||||
value(match.allocation().getPickedPackageCount()) == value(match.allocation().getPlannedPackageCount()));
|
||||
int accepted = 0;
|
||||
@@ -511,7 +521,6 @@ public class ReceivingContainerService {
|
||||
}
|
||||
List<InboundRef> group = matches.stream().map(DropshipPalletRef::ref).distinct().toList();
|
||||
List<String> inboundCodes = group.stream().map(value -> value.line().getInboundCode()).distinct().sorted().toList();
|
||||
int total = matches.stream().mapToInt(value -> value(value.allocation().getPlannedPackageCount())).sum();
|
||||
int received = matches.stream().mapToInt(value -> value(value.allocation().getPickedPackageCount())).sum();
|
||||
InboundRef first = group.get(0);
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(first);
|
||||
@@ -885,6 +894,10 @@ public class ReceivingContainerService {
|
||||
|| "LOCKED".equals(pallet.getAssignmentStatus()));
|
||||
}
|
||||
|
||||
private void requirePalletCapacity(long packageCount) {
|
||||
if (packageCount > MAX_PACKAGES_PER_PALLET) throw conflict(PALLET_CAPACITY_MESSAGE);
|
||||
}
|
||||
|
||||
private void requireMaterializedPlans(List<InboundPlan> containerPlans) {
|
||||
for (InboundPlan plan : containerPlans) {
|
||||
int expected = plan.getPalletCount() == null ? 0 : plan.getPalletCount();
|
||||
|
||||
-2
@@ -126,7 +126,6 @@ public class InboundPlanService {
|
||||
if (order.version() != request.expectedOrderVersion()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "尾程订单版本已变化,请刷新后重试");
|
||||
}
|
||||
if (order.detained()) throw new ResponseStatusException(HttpStatus.CONFLICT, "暂扣尾程订单不能生成入库计划");
|
||||
if (!Objects.equals(containerWarehouseId, trimToNull(order.overseasWarehouseId()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜接柜仓冻结事实已变化,请刷新后重试");
|
||||
}
|
||||
@@ -269,7 +268,6 @@ public class InboundPlanService {
|
||||
boolean planned = trimToNull(row.planNo()) != null;
|
||||
if (!planned) {
|
||||
if (trimToNull(row.inboundCode()) == null) blockers.add("MISSING_INBOUND_CODE");
|
||||
if (row.detained()) blockers.add("ORDER_DETAINED");
|
||||
if (!("PENDING_ASSIGNMENT".equals(row.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(row.currentStatus()))) blockers.add("ORDER_STATUS_NOT_READY");
|
||||
if (containerNo == null || !Objects.equals(containerNo, trimToNull(row.containerNo()))) {
|
||||
|
||||
+6
@@ -60,6 +60,12 @@ public interface InventoryCommandMapper {
|
||||
@Param("sourceIdentity") String sourceIdentity,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
|
||||
"WHERE p.customer_id=#{customerId} AND b.inbound_order_id=#{inboundOrderId} " +
|
||||
"AND p.available_packages>0 ORDER BY p.id FOR UPDATE")
|
||||
List<InventoryPosition> lockInboundAvailablePositions(@Param("customerId") String customerId,
|
||||
@Param("inboundOrderId") Long inboundOrderId);
|
||||
|
||||
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
|
||||
"WHERE p.customer_id=#{customerId} AND b.inbound_order_id=#{inboundOrderId} " +
|
||||
"AND p.hold_packages>0 ORDER BY p.id FOR UPDATE")
|
||||
|
||||
+11
@@ -142,6 +142,17 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void holdInboundOrderInventory(String customerId, String operatorId, long inboundOrderId,
|
||||
String businessOrderNo, String remark) {
|
||||
for (InventoryPosition position : commands.lockInboundAvailablePositions(customerId, inboundOrderId)) {
|
||||
long packages = value(position.getAvailablePackages());
|
||||
if (packages == 0) continue;
|
||||
hold(customerId, operatorId, new InventoryCommandRequests.Move(position.getId(), packages,
|
||||
businessOrderNo, null, null, "ORDER_HOLD:" + inboundOrderId + ":" + position.getId(), remark));
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void releaseInboundOrderHold(String customerId, String operatorId, long inboundOrderId,
|
||||
String businessOrderNo, String remark) {
|
||||
|
||||
+12
@@ -15,6 +15,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -51,6 +52,7 @@ public class LastlegOrderCommandService {
|
||||
@Resource private LastlegOrderManualNodeHistoryMapper manualNodeHistory;
|
||||
@Resource private LastlegOrderOperationLogMapper operationLogs;
|
||||
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
@@ -69,6 +71,9 @@ public class LastlegOrderCommandService {
|
||||
if (Boolean.TRUE.equals(order.getDetained()) == held) {
|
||||
throw conflict(held ? "尾程订单已暂扣" : "尾程订单未处于暂扣状态");
|
||||
}
|
||||
if (held && !"INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
|
||||
throw conflict("货物完成入库后才能暂扣");
|
||||
}
|
||||
if (held && (positive(order.getPickedPackageCount()) || positive(order.getShippedPackageCount())
|
||||
|| text(order.getOutboundOrderNo()) != null)) {
|
||||
throw conflict("尾程订单已进入出库流程,不能新增暂扣");
|
||||
@@ -79,6 +84,13 @@ public class LastlegOrderCommandService {
|
||||
if (orders.updateHold(order.getCustomerId(), orderId, held, chargeable, reason, version, now) != 1) {
|
||||
throw conflict("尾程订单版本已变化,请刷新后重试");
|
||||
}
|
||||
if (held) {
|
||||
inventory.holdInboundOrderInventory(order.getCustomerId(), operatorId, orderId,
|
||||
order.getOrderNo(), reason);
|
||||
} else {
|
||||
inventory.releaseInboundOrderHold(order.getCustomerId(), operatorId, orderId,
|
||||
order.getOrderNo(), reason);
|
||||
}
|
||||
int nextVersion = version + 1;
|
||||
LastlegOrderCommandDtos.HoldResponse response = new LastlegOrderCommandDtos.HoldResponse(
|
||||
orderId, held, chargeable, held ? reason : null, nextVersion);
|
||||
|
||||
@@ -48,7 +48,6 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -79,7 +78,6 @@ public class LastlegOrderService {
|
||||
@Resource private InboundPlanCartonMapper cartonMapper;
|
||||
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||||
@Resource private WarehouseResourceGateway warehouseResourceGateway;
|
||||
@@ -387,47 +385,6 @@ public class LastlegOrderService {
|
||||
return fee;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
|
||||
String operatorName) {
|
||||
setDetained(customerId, orderId, request, null, operatorName);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setDetained(String customerId, long orderId, LastlegOrderRequests.SetDetained request,
|
||||
String operatorId, String operatorName) {
|
||||
LastlegOrder current = findOwned(customerId, orderId);
|
||||
LastlegOrder order = orderMapper.lockById(current.getCustomerId(), orderId);
|
||||
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
||||
boolean detained = request.detained();
|
||||
if (Boolean.TRUE.equals(order.getDetained()) == detained) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, detained ? "Order is already held" : "Order is not held");
|
||||
}
|
||||
if (detained && "INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed inbound orders cannot be newly held");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
order.setDetained(detained);
|
||||
order.setUpdatedAt(now);
|
||||
orderMapper.updateById(order);
|
||||
if (!detained && "INBOUND_COMPLETED".equals(order.getCurrentStatus())) {
|
||||
inventory.releaseInboundOrderHold(order.getCustomerId(), operatorName, orderId, order.getOrderNo(), request.remark().trim());
|
||||
}
|
||||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||||
operation.setId(IdWorker.getId());
|
||||
operation.setCustomerId(order.getCustomerId());
|
||||
operation.setOrderId(orderId);
|
||||
operation.setOperationNode(detained ? "暂扣" : "解除暂扣");
|
||||
operation.setOperatorName(operatorName);
|
||||
operation.setResult(request.remark().trim());
|
||||
operation.setCreatedAt(now);
|
||||
operationLogMapper.insert(operation);
|
||||
if (StringUtils.hasText(order.getSourceInboundId())) {
|
||||
detainedOutbox.publish(order, detained, request.remark().trim(), operatorId, operatorName,
|
||||
now.toInstant(ZoneOffset.UTC), operation.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setPalletMergeRequired(String customerId, long orderId,
|
||||
LastlegOrderRequests.SetPalletMergeRequired request,
|
||||
|
||||
@@ -126,21 +126,21 @@ paths:
|
||||
/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk:
|
||||
post:
|
||||
summary: FBA 入仓码按箱数收货
|
||||
description: 权限 lastleg:pda:operate;只允许普通 FBA。packageCount 是该托现场累计箱数,服务端按当前柜+FBA目的仓稳定分摊到全部关联入仓码;超出计划部分只记差异,不生成可用库存。
|
||||
description: 权限 lastleg:pda:operate;只允许普通 FBA。packageCount 是该托现场累计箱数,单托最多 66 箱;服务端按当前柜+FBA目的仓稳定分摊到全部关联入仓码;超出计划但未超出单托上限的部分只记差异,不生成可用库存。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingBulk' } } } }
|
||||
responses: { '200': { description: 本次与累计实收进度,超收返回 DIFFERENCE_RECORDED, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingReceiptApiResponse' } } } }, '409': { description: 非普通FBA、累计数回退、版本变化或托盘不匹配 } }
|
||||
responses: { '200': { description: 本次与累计实收进度,超收返回 DIFFERENCE_RECORDED, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingReceiptApiResponse' } } } }, '409': { description: 非普通FBA、累计数回退、单托超过66箱、版本变化或托盘不匹配 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/receipts/carton:
|
||||
post:
|
||||
summary: 海外仓入仓码逐箱收货
|
||||
description: 权限 lastleg:pda:operate;只允许 destinationType=OVERSEAS,拒绝跨柜、重复箱和不匹配托盘。
|
||||
description: 权限 lastleg:pda:operate;只允许 destinationType=OVERSEAS,单托最多 66 箱,拒绝跨柜、重复箱和不匹配托盘。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCarton' } } } }
|
||||
responses: { '200': { description: 本次与累计实收进度, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingReceiptApiResponse' } } } }, '409': { description: FBA逐箱、跨柜、重复箱或托盘不匹配 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/receipts/pallet:
|
||||
post:
|
||||
summary: 一件代发按完整来源托盘收货
|
||||
description: 权限 lastleg:pda:operate;只允许 ONE_CLICK_DROPSHIP,扫描 DP 或该托任一箱码,一次接收跨客户完整 DP;所有客户事件分片清单一致且齐全后才允许收货。
|
||||
description: 权限 lastleg:pda:operate;只允许 ONE_CLICK_DROPSHIP,扫描 DP 或该托任一箱码,一次接收跨客户完整 DP;单托最多 66 箱,所有客户事件分片清单一致且齐全后才允许收货。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingWholePallet' } } } }
|
||||
responses: { '200': { description: 本次与累计实收进度, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingReceiptApiResponse' } } } }, '404': { description: 来源托盘不属于当前柜 }, '409': { description: 非一件代发、来源托盘重复或已操作 } }
|
||||
@@ -232,7 +232,7 @@ paths:
|
||||
/api/platform/v1/last-leg/orders/{id}/hold:
|
||||
put:
|
||||
summary: 设置或解除尾程订单暂扣
|
||||
description: 权限 platform:overseas-warehouse:last-mile-order:edit;使用订单版本和 Idempotency-Key 原子写入计费标记、原因及不可变历史。已进入出库流程的订单不得新增暂扣;暂扣只作用于当前票,不影响同柜其他订单。
|
||||
description: 权限 platform:overseas-warehouse:last-mile-order:edit;使用订单版本和 Idempotency-Key 原子写入计费标记、原因及不可变历史。仅完成入库且尚未进入出库流程的订单允许新增暂扣;解除暂扣不受该状态限制。暂扣只作用于当前票并仅阻断排车和出库,不影响入库计划或同柜其他订单。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
@@ -245,7 +245,7 @@ paths:
|
||||
schema: { $ref: '#/components/schemas/LastlegOrderHoldRequest' }
|
||||
responses:
|
||||
'200': { description: 暂扣状态已更新, content: { application/json: { schema: { type: object, properties: { code: { type: string }, data: { $ref: '#/components/schemas/LastlegOrderHoldResponse' } } } } } }
|
||||
'409': { description: 版本冲突、幂等键冲突、重复状态或订单已进入出库流程 }
|
||||
'409': { description: 版本冲突、幂等键冲突、重复状态、尚未完成入库或订单已进入出库流程 }
|
||||
/api/platform/v1/last-leg/orders/{id}/manual-nodes:
|
||||
get:
|
||||
summary: 查询尾程订单人工展示节点
|
||||
@@ -496,7 +496,7 @@ paths:
|
||||
/api/platform/v1/last-leg/inbound-plans/manual:
|
||||
post:
|
||||
summary: 平台按单票手工生成入库计划
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:create;按 customerId 与 orderId 锁定单票,校验订单版本、状态、暂扣状态、真实入仓码、客户归属及有效公共接柜仓。预计到仓日期由服务端锁定读取来源柜全部订单的 estimatedWarehouseAt,缺失或同柜不一致时拒绝生成,不接受调用方提交。一次只生成当前票的独立计划,不改变同柜其他订单;命令原子且幂等。
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:create;按 customerId 与 orderId 锁定单票,校验订单版本、状态、真实入仓码、客户归属及有效公共接柜仓。暂扣只限制排车和出库,不阻断入库计划。预计到仓日期由服务端锁定读取来源柜全部订单的 estimatedWarehouseAt,缺失或同柜不一致时拒绝生成,不接受调用方提交。一次只生成当前票的独立计划,不改变同柜其他订单;命令原子且幂等。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: Idempotency-Key, in: header, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
|
||||
@@ -508,7 +508,7 @@ paths:
|
||||
responses:
|
||||
'200': { description: 统一 ApiResponse 包装的入库计划详情 }
|
||||
'400': { description: 客户、入仓码、仓库主数据或请求字段无效 }
|
||||
'409': { description: 订单版本、状态、暂扣状态、柜级预计到仓时间、重复计划或幂等键冲突 }
|
||||
'409': { description: 订单版本、状态、柜级预计到仓时间、重复计划或幂等键冲突 }
|
||||
/api/platform/v1/last-leg/inbound-plans/convert-container:
|
||||
post:
|
||||
summary: 按整柜批量生成剩余入库计划
|
||||
@@ -2044,7 +2044,7 @@ components:
|
||||
properties:
|
||||
inboundCode: { type: string, minLength: 1 }
|
||||
palletCode: { type: string, minLength: 1 }
|
||||
packageCount: { type: integer, minimum: 1 }
|
||||
packageCount: { type: integer, minimum: 1, maximum: 66 }
|
||||
expectedPalletVersion: { type: integer, minimum: 0, nullable: true }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
ReceivingCarton:
|
||||
|
||||
+2
@@ -153,6 +153,8 @@ class ReceivingContainerContractTest {
|
||||
assertTrue(yaml.contains("enum: [LICENSE_PLATE, CONTAINER_SEAL, DOOR_OPEN]"));
|
||||
assertTrue(yaml.contains("photos: { type: array, minItems: 3, maxItems: 3"));
|
||||
assertTrue(yaml.contains("required: [role, fileId, downloadPath]"));
|
||||
assertTrue(yaml.contains("packageCount: { type: integer, minimum: 1, maximum: 66 }"));
|
||||
assertTrue(receivingPaths.contains("单托最多 66 箱"));
|
||||
assertTrue(yaml.contains("overseasWarehouseId: { type: string"));
|
||||
assertTrue(yaml.contains("overseasWarehouseName: { type: string"));
|
||||
assertFalse(receivingPaths.contains("photoFileIds"));
|
||||
|
||||
+73
@@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -422,6 +423,55 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
verifyNoInteractions(workflow);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fbaPalletAccepts66PackagesAndRejects67BeforeReceivingStarts() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
InboundPlanLine fbaLine = line("FBA");
|
||||
fbaLine.setRequiredPackageCount(66);
|
||||
when(lines.selectList(any())).thenReturn(List.of(fbaLine));
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(detail("SKU-1", 66)));
|
||||
when(skuReceipts.listLineSku("100", 10L, "SKU-1")).thenReturn(List.of());
|
||||
stubReservedFbaPallet();
|
||||
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(4));
|
||||
when(fbaPalletCounts.insert(any(ReceivingFbaPalletCount.class))).thenReturn(1);
|
||||
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
|
||||
var accepted = service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 66, "capacity-66"));
|
||||
|
||||
assertThat(accepted.acceptedPackages()).isEqualTo(66);
|
||||
|
||||
org.mockito.Mockito.reset(sessions, workflow, warehouse);
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 67, "capacity-67")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
verifyNoInteractions(sessions, workflow, warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasCartonIsRejectedWhenPhysicalPalletAlreadyContains66Packages() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "IN-1#67"));
|
||||
InboundPlanPallet reserved = planPallet();
|
||||
reserved.setPalletNo("PAL-1"); reserved.setWarehousePalletId(30L); reserved.setWarehouseLocationId(40L);
|
||||
reserved.setWarehouseResourceLockId(50L); reserved.setAssignmentStatus("RESERVED");
|
||||
when(pallets.selectList(any())).thenReturn(List.of(reserved));
|
||||
when(warehouse.resolveReservedPallet("PAL-1", "100", 100L, 30L, 40L, 50L))
|
||||
.thenReturn(resource(100L, null));
|
||||
when(cartons.selectCount(any())).thenReturn(66L);
|
||||
|
||||
assertThatThrownBy(() -> service.receiveCarton("7", "receiver",
|
||||
new ReceivingContainerRequests.CartonReceive("IN-1#67", "PAL-1", "carton-capacity")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
|
||||
verify(workflow, never()).validateCartonResolved(any(), any(), any(), any(), any());
|
||||
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossContainerCartonAndMismatchedPalletAreRejectedBeforeReservation() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
@@ -950,6 +1000,29 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
verifyNoInteractions(warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropshipWholePalletOver66PackagesIsRejectedBeforePicking() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
LastlegOrder dropship = order("OVERSEAS"); dropship.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.selectById(1L)).thenReturn(dropship);
|
||||
InboundPlanPallet pallet = planPallet();
|
||||
pallet.setPalletNo("DP-260909-0067"); pallet.setSourcePalletNo("DP-260909-0067");
|
||||
pallet.setWarehousePalletId(30L);
|
||||
InboundPlanPalletAllocation pending = new InboundPlanPalletAllocation();
|
||||
pending.setId(67L); pending.setCustomerId("100"); pending.setPlanId(2L);
|
||||
pending.setLineId(10L); pending.setPalletId(20L); pending.setPlannedPackageCount(67);
|
||||
when(pallets.selectList(any())).thenReturn(List.of(pallet));
|
||||
when(allocations.selectList(any())).thenReturn(List.of(pending));
|
||||
stubCompleteManifest("DP-260909-0067", 67);
|
||||
|
||||
assertThatThrownBy(() -> service.receiveWholePallet("7", "receiver",
|
||||
new ReceivingContainerRequests.WholePalletReceive("DP-260909-0067", "whole-capacity")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
|
||||
verify(workflow, never()).pickWholePallet(any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void domesticLoadingPhotoDownloadIsScopedToTheCurrentPhysicalContainer() {
|
||||
ReceivingContainerSession current = session("CONT-A", "SIGNED");
|
||||
|
||||
+16
@@ -85,6 +85,22 @@ class ContainerInboundPlanPreviewTest {
|
||||
assertFalse(result.ready());
|
||||
}
|
||||
|
||||
@Test
|
||||
void heldHistoricalOrderRemainsReadyBecauseHoldOnlyBlocksOutbound() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-HELD")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", 1L, "LM-HELD", "FO-1",
|
||||
"测试客户", "FIRST_LEG", "FBA", "IN-1", "PENDING_ASSIGNMENT", null,
|
||||
"MSCU6639870", 1, 10, new BigDecimal("1.0000"), "193000000000200003", "US-LAX-01",
|
||||
"洛杉矶仓", "ACY2", null, null, "Amazon ACY2", 0, true, 1,
|
||||
null, null, null, null, 1)));
|
||||
|
||||
var result = service.previewContainer("BOOKING-HELD");
|
||||
|
||||
assertEquals(true, result.ready());
|
||||
assertEquals("READY", result.groups().get(0).orders().get(0).readiness());
|
||||
assertEquals(List.of(), result.groups().get(0).orders().get(0).blockers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropshipPreviewKeepsUnderlyingFbaFactSeparateFromItsDisplayCategory() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-DROPSHIP")).thenReturn(List.of(
|
||||
|
||||
+9
-7
@@ -352,21 +352,23 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesManualPlanForHeldOrderBeforeWriting() {
|
||||
void createsManualPlanForHeldHistoricalOrderBecauseHoldOnlyBlocksOutbound() {
|
||||
SourceOrderSnapshot held = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, "WH-A", "仓A", 1, 0, true, 5);
|
||||
"WH-A", "WH-A", "仓A", 1, 0, true, 5);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(1), eq(false), eq(5), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
var result = service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 5,
|
||||
1, "STANDARD", false, false, false, null),
|
||||
"manual-held", "900"));
|
||||
"manual-held", "900");
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(1), eq(false), eq(5), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -280,6 +280,23 @@ class InventoryCommandServiceTest {
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsAllAvailablePositionsForOneInboundOrder() {
|
||||
InventoryBalance balance = balance(200L, 5);
|
||||
InventoryPosition position = position(201L, 200L, 5);
|
||||
when(commands.lockInboundAvailablePositions("100", 101L)).thenReturn(List.of(position));
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
|
||||
service.holdInboundOrderInventory("100", "7", 101L, "ORD-1", "质检暂扣");
|
||||
|
||||
assertEquals(0L, position.getAvailablePackages());
|
||||
assertEquals(5L, position.getHoldPackages());
|
||||
assertEquals(0L, balance.getAvailablePackages());
|
||||
assertEquals(5L, balance.getHoldPackages());
|
||||
verify(ledgers).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayUsesPersistedResultWithoutMutatingInventoryAgain() throws Exception {
|
||||
var request = stockIn("IN-1", "stock-in-1", 12, 24);
|
||||
|
||||
+23
@@ -16,6 +16,15 @@ class MerchantOverseasOpenApiContractTest {
|
||||
}
|
||||
|
||||
for (String path : new String[] {
|
||||
"/api/merchant/v1/last-leg/orders:",
|
||||
"/api/merchant/v1/last-leg/orders/{id}:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}/confirm-measurement:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/inventory-candidates:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/{discardNo}:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/{discardNo}/cancel:",
|
||||
"/api/merchant/v1/last-leg/overseas-procurements:",
|
||||
"/api/merchant/v1/last-leg/overseas-procurements/{orderNo}:",
|
||||
"/api/merchant/v1/last-leg/us-truck-orders:",
|
||||
@@ -35,6 +44,20 @@ class MerchantOverseasOpenApiContractTest {
|
||||
.contains("merchant:cross-border:pallet-shipping:list")
|
||||
.contains("merchant:cross-border:pallet-shipping:create")
|
||||
.contains("merchant:cross-border:pallet-shipping:edit")
|
||||
.contains("merchant:overseas-warehouse:last-mile-order:list")
|
||||
.contains("merchant:overseas-warehouse:transfer:list")
|
||||
.contains("merchant:overseas-warehouse:transfer:create")
|
||||
.contains("merchant:overseas-warehouse:transfer:confirm")
|
||||
.contains("merchant:overseas-warehouse:discard:list")
|
||||
.contains("merchant:overseas-warehouse:discard:create")
|
||||
.contains("merchant:overseas-warehouse:discard:cancel")
|
||||
.contains("merchant:overseas-warehouse:relabel:list")
|
||||
.contains("merchant:overseas-warehouse:relabel:create")
|
||||
.contains("merchant:overseas-warehouse:relabel:files")
|
||||
.contains("merchant:overseas-warehouse:relabel:confirm")
|
||||
.contains("merchant:overseas-warehouse:return-order:list")
|
||||
.contains("merchant:overseas-warehouse:return-order:create")
|
||||
.contains("merchant:overseas-warehouse:return-order:relabel")
|
||||
.contains("merchant:warehouse:inbound:list")
|
||||
.contains("#/components/schemas/ProcurementDetailResponse")
|
||||
.contains("#/components/schemas/TruckDetailResponse")
|
||||
|
||||
+37
@@ -12,6 +12,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -48,6 +49,7 @@ class LastlegOrderCommandServiceTest {
|
||||
@Mock LastlegOrderManualNodeHistoryMapper manualNodeHistory;
|
||||
@Mock LastlegOrderOperationLogMapper operationLogs;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock InventoryCommandService inventory;
|
||||
LastlegOrderCommandService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -60,12 +62,14 @@ class LastlegOrderCommandServiceTest {
|
||||
ReflectionTestUtils.setField(service, "manualNodeHistory", manualNodeHistory);
|
||||
ReflectionTestUtils.setField(service, "operationLogs", operationLogs);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsOnlySelectedOrderWithVersionAndImmutableHistory() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
order.setCurrentStatus("INBOUND_COMPLETED");
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
when(orders.updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3),
|
||||
@@ -80,9 +84,42 @@ class LastlegOrderCommandServiceTest {
|
||||
verify(orders).updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3), any());
|
||||
verify(holdHistory).insert(any(LastlegOrderHoldHistory.class));
|
||||
verify(commands).insert(any(LastlegOrderCommandRecord.class));
|
||||
verify(inventory).holdInboundOrderInventory("100", "900", 11L, "LL-11", "海关查验");
|
||||
verify(orders, never()).lockContainerOrders(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingHoldMovesOrderInventoryBackToAvailable() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
order.setCurrentStatus("INBOUND_COMPLETED");
|
||||
order.setDetained(true);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
when(orders.updateHold(eq("100"), eq(11L), eq(false), eq(false), eq("质检通过"), eq(3),
|
||||
any(LocalDateTime.class))).thenReturn(1);
|
||||
|
||||
service.hold("100", 11L, "release-11",
|
||||
new LastlegOrderCommandDtos.HoldRequest(false, false, "质检通过", 3), "900", "管理员");
|
||||
|
||||
verify(inventory).releaseInboundOrderHold("100", "900", 11L, "LL-11", "质检通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNewHoldBeforeInboundCompletion() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.hold("100", 11L,
|
||||
"hold-before-inbound", new LastlegOrderCommandDtos.HoldRequest(true, false, "待入库", 3),
|
||||
"900", "管理员"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
assertEquals("货物完成入库后才能暂扣", error.getReason());
|
||||
verify(orders, never()).updateHold(anyString(), anyLong(), anyBoolean(), anyBoolean(),
|
||||
nullable(String.class), anyInt(), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleHoldWithoutAnyWrite() {
|
||||
LastlegOrder order = order(11L, 4);
|
||||
|
||||
@@ -49,7 +49,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -69,7 +68,6 @@ class LastlegOrderServiceTest {
|
||||
@Mock InboundPlanCartonMapper cartons;
|
||||
@Mock FbaWarehouseValidator fbaWarehouses;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Mock ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||||
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
||||
@@ -116,7 +114,6 @@ class LastlegOrderServiceTest {
|
||||
Long.parseLong(invocation.getArgument(0)), invocation.getArgument(1), invocation.getArgument(2),
|
||||
"US", "CA"));
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
|
||||
ReflectionTestUtils.setField(service, "containerArrivalTimeOperations", containerArrivalTimeOperations);
|
||||
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
|
||||
@@ -559,40 +556,6 @@ class LastlegOrderServiceTest {
|
||||
assertEquals("NYEXX2609060063", result.records().get(2).inboundCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
|
||||
when(orders.selectOne(any())).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(true, "等待质检"), "operator");
|
||||
|
||||
assertTrue(order.getDetained());
|
||||
verify(orders).updateById(order);
|
||||
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
|
||||
verify(operations).insert(operation.capture());
|
||||
assertEquals("暂扣", operation.getValue().getOperationNode());
|
||||
assertEquals("等待质检", operation.getValue().getResult());
|
||||
verify(detainedOutbox).publish(eq(order), eq(true), eq("等待质检"), isNull(), eq("operator"), any(), anyLong());
|
||||
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingAfterInboundMovesAllOrderHeldInventoryToAvailable() {
|
||||
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IP-1");
|
||||
order.setOrderNo("LM-1");
|
||||
order.setDetained(true);
|
||||
when(orders.selectOne(any())).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(false, "质检通过"), "operator");
|
||||
|
||||
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
|
||||
assertEquals(false, order.getDetained());
|
||||
verify(detainedOutbox).publish(eq(order), eq(false), eq("质检通过"), isNull(), eq("operator"), any(), anyLong());
|
||||
}
|
||||
|
||||
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(id);
|
||||
|
||||
Reference in New Issue
Block a user