fix(transfer): preserve inbound ownership and expose durable receipt recovery
This commit is contained in:
+1
-1
@@ -968,7 +968,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
LastlegOrder transferOrder = orders.selectById(transfer.getLastlegOrderId());
|
||||
if (transferOrder != null && customerId.equals(transferOrder.getCustomerId())) {
|
||||
String transferOrderBefore = transferOrder.getCurrentStatus();
|
||||
transferOrder.setCurrentStatus(transferStatus);
|
||||
if (!existingStockTransfer) transferOrder.setCurrentStatus(transferStatus);
|
||||
transferOrder.setFbaWarehouseCode(transfer.getFbaWarehouseCode());
|
||||
transferOrder.setRelabelStatus("COMPLETED");
|
||||
transferOrder.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transferStatus + ";RELABEL:COMPLETED");
|
||||
|
||||
+6
@@ -26,6 +26,11 @@ public final class WarehouseTransferResponses {
|
||||
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
|
||||
Long palletId, String palletNo, int packageCount, String status,
|
||||
Long destinationInventoryPositionId, String skuCode) {}
|
||||
public record ReceiptTarget(String warehouseId, String warehouseCode, String warehouseName,
|
||||
Long palletId, String palletNo, Long locationId, String locationCode,
|
||||
String fbaId, String fbaCode, String fbaName) {}
|
||||
public record ReceiptCommand(Long sourceInventoryPositionId, String status, String requestHash,
|
||||
WarehouseTransferRequests.Receipt request, ReceiptTarget target) {}
|
||||
public record Summary(Long id, String transferNo, String inboundCode, Long lastlegOrderId,
|
||||
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
@@ -51,6 +56,7 @@ public final class WarehouseTransferResponses {
|
||||
List<SourceOrder> sourceOrders, List<Item> items, List<Carton> cartons,
|
||||
List<Pallet> pallets, List<RelabelBinding> relabelBindings,
|
||||
List<Allocation> allocations, List<MeasurementPhoto> measurementPhotos,
|
||||
List<ReceiptCommand> receiptCommands,
|
||||
LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime actualShipAt, LocalDateTime createdAt) {}
|
||||
}
|
||||
|
||||
+5
@@ -8,6 +8,11 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface WarehouseTransferReceiptCommandMapper extends BaseMapper<WarehouseTransferReceiptCommand> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_receipt_command WHERE customer_id=#{customerId} " +
|
||||
"AND transfer_order_id=#{transferId} ORDER BY id")
|
||||
java.util.List<WarehouseTransferReceiptCommand> listTransfer(@Param("customerId") String customerId,
|
||||
@Param("transferId") Long transferId);
|
||||
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_receipt_command WHERE customer_id=#{customerId} " +
|
||||
"AND transfer_order_id=#{transferId} AND source_inventory_position_id=#{sourceId} LIMIT 1 FOR UPDATE")
|
||||
WarehouseTransferReceiptCommand lockSource(@Param("customerId") String customerId,
|
||||
|
||||
+71
-3
@@ -54,6 +54,72 @@ public class WarehouseTransferReceiptCoordinator {
|
||||
private record ResolvedTarget(Target target, String palletStatus) { }
|
||||
private record Admission(WarehouseTransferOrder transfer, boolean replay) { }
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional(readOnly = true)
|
||||
public java.util.List<WarehouseTransferResponses.ReceiptCommand> receiptCommands(String customerId, long transferId) {
|
||||
var transfer = transfers.selectById(transferId);
|
||||
if (transfer == null || !Objects.equals(customerId, transfer.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Warehouse transfer order not found");
|
||||
}
|
||||
var rows = commands.listTransfer(customerId, transferId);
|
||||
require(rows != null, "持久实收命令读取失败,请联系管理员核查");
|
||||
java.util.Set<Long> sources = new java.util.HashSet<>();
|
||||
return rows.stream().map(row -> {
|
||||
require(row != null && Objects.equals(customerId, row.getCustomerId()) && Objects.equals(transferId, row.getTransferOrderId())
|
||||
&& row.getSourceInventoryPositionId() != null && row.getSourceInventoryPositionId() > 0
|
||||
&& sources.add(row.getSourceInventoryPositionId())
|
||||
&& row.getStatus() != null && Set.of("PREPARED", "RESERVED", "COMPLETED").contains(row.getStatus()),
|
||||
"持久实收命令归属或状态无效,请联系管理员核查");
|
||||
var request = readSnapshot(row.getRequestJson(), WarehouseTransferRequests.Receipt.class);
|
||||
var target = readSnapshot(row.getTargetJson(), Target.class);
|
||||
require(Objects.equals(request.sourceInventoryPositionId(), row.getSourceInventoryPositionId())
|
||||
&& text(request.idempotencyKey()) != null && text(request.idempotencyKey()).equals(row.getIdempotencyKey())
|
||||
&& Objects.equals(hash(request), row.getRequestHash()), "持久实收原请求损坏,请联系管理员核查");
|
||||
long warehouseId = positive(target.warehouseId(), "持久实收目标损坏,请联系管理员核查");
|
||||
require(target.palletId() != null && target.palletId() > 0 && target.locationId() != null && target.locationId() > 0
|
||||
&& text(target.warehouseCode()) != null && text(target.warehouseName()) != null
|
||||
&& text(target.palletNo()) != null && text(target.locationCode()) != null && text(target.fbaCode()) != null
|
||||
&& text(target.fbaName()) != null && positive(target.fbaId(), "持久实收目标损坏,请联系管理员核查") > 0
|
||||
&& Objects.equals(row.getTargetWarehouseId(), warehouseId)
|
||||
&& Objects.equals(row.getTargetPalletId(), target.palletId())
|
||||
&& Objects.equals(row.getTargetLocationId(), target.locationId())
|
||||
&& Objects.equals(row.getTargetHash(), hash(new Binding(target.warehouseId(), target.palletId(), target.locationId(), target.fbaCode())))
|
||||
&& positive(request.destinationOverseasWarehouseId(), "持久实收原请求损坏,请联系管理员核查") == warehouseId
|
||||
&& Objects.equals(request.destinationPalletId(), target.palletId())
|
||||
&& Objects.equals(text(request.destinationPalletNo()), target.palletNo())
|
||||
&& Objects.equals(text(request.destinationWarehouseCode()), target.fbaCode())
|
||||
&& (text(request.destinationWarehouseId()) == null || positive(request.destinationWarehouseId(),
|
||||
"持久实收原请求损坏,请联系管理员核查") == positive(target.fbaId(), "持久实收目标损坏,请联系管理员核查"))
|
||||
&& (request.warehouseLocationId() == null || Objects.equals(request.warehouseLocationId(), target.locationId()))
|
||||
&& ("PREPARED".equals(row.getStatus()) || row.getResourceLockId() != null && row.getResourceLockId() > 0),
|
||||
"持久实收目标损坏,请联系管理员核查");
|
||||
if ("COMPLETED".equals(row.getStatus())) {
|
||||
var receipt = receipts.findSource(customerId, transferId, row.getSourceInventoryPositionId());
|
||||
require(receipt != null && Objects.equals(receipt.getCustomerId(), customerId)
|
||||
&& Objects.equals(receipt.getTransferOrderId(), transferId)
|
||||
&& Objects.equals(receipt.getRequestHash(), row.getRequestHash())
|
||||
&& Objects.equals(receipt.getIdempotencyKey(), row.getIdempotencyKey()),
|
||||
"持久实收完成凭据缺失,请联系管理员核查");
|
||||
}
|
||||
return new WarehouseTransferResponses.ReceiptCommand(row.getSourceInventoryPositionId(), row.getStatus(),
|
||||
row.getRequestHash(), request, new WarehouseTransferResponses.ReceiptTarget(target.warehouseId(),
|
||||
target.warehouseCode(), target.warehouseName(), target.palletId(), target.palletNo(),
|
||||
target.locationId(), target.locationCode(), target.fbaId(), target.fbaCode(), target.fbaName()));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private <T> T readSnapshot(String payload, Class<T> type) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.JsonNode tree = objectMapper.reader().with(com.fasterxml.jackson.core.JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
|
||||
.with(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS).readTree(payload);
|
||||
require(tree != null && tree.isObject(), "持久实收快照损坏,请联系管理员核查");
|
||||
var fields = java.util.Arrays.stream(type.getRecordComponents()).map(java.lang.reflect.RecordComponent::getName)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
tree.fieldNames().forEachRemaining(field -> require(fields.contains(field), "持久实收快照含未知字段,请联系管理员核查"));
|
||||
return objectMapper.treeToValue(tree, type);
|
||||
} catch (ResponseStatusException failure) { throw failure; }
|
||||
catch (Exception failure) { throw new ResponseStatusException(HttpStatus.CONFLICT, "持久实收快照损坏,请联系管理员核查"); }
|
||||
}
|
||||
|
||||
public WarehouseTransferResponses.Detail receive(String customerId, String transferNo, String operatorId,
|
||||
WarehouseTransferRequests.Receipt request, Supplier<WarehouseTransferResponses.Detail> replay,
|
||||
Function<Context, WarehouseTransferResponses.Detail> stockWork) {
|
||||
@@ -99,9 +165,11 @@ public class WarehouseTransferReceiptCoordinator {
|
||||
if (current.replay()) return replay.get();
|
||||
require("RESERVED".equals(owned.getStatus()) && Objects.equals(owned.getResourceLockId(), lockId),
|
||||
"Receipt has no active reserved resource");
|
||||
WarehouseTransferResponses.Detail result = stockWork.apply(new Context(target, lockId, owned.getId()));
|
||||
owned.setStatus("COMPLETED"); owned.setUpdatedAt(LocalDateTime.now()); commands.updateById(owned);
|
||||
return result;
|
||||
stockWork.apply(new Context(target, lockId, owned.getId()));
|
||||
owned.setStatus("COMPLETED"); owned.setUpdatedAt(LocalDateTime.now());
|
||||
require(commands.updateById(owned) == 1, "Receipt completion changed concurrently");
|
||||
// Read inside the same transaction after completion, so the response cannot leak the earlier RESERVED view.
|
||||
return replay.get();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -601,6 +601,7 @@ public class WarehouseTransferService {
|
||||
transfer.getRelabelContactPhone(), transfer.getRelabelDeliveryMethod(),
|
||||
transfer.getReviewDecision(), transfer.getReviewRemark(), transfer.getReviewedBy(),
|
||||
transfer.getReviewedAt(), sources, items, cartons, pallets, bindings, allocations, photos,
|
||||
receiptCoordinator.receiptCommands(customerId, transfer.getId()),
|
||||
transfer.getCustomerConfirmedAt(), transfer.getOutboundAt(), transfer.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -1129,8 +1130,7 @@ public class WarehouseTransferService {
|
||||
transfer.setAllocationIdempotencyKey(key);
|
||||
transfer.setAllocationRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus(
|
||||
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? INBOUND_COMPLETED : "WAIT_OUTBOUND");
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus(INBOUND_COMPLETED);
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
order.setRelabelOrderNo(transfer.getRelabelNo()); order.setRelabelStatus("PENDING_REVIEW");
|
||||
}
|
||||
@@ -1203,7 +1203,7 @@ public class WarehouseTransferService {
|
||||
transfer.setOutboundRequestHash(requestHash); transfer.setOutboundAt(now); transfer.setUpdatedAt(now);
|
||||
transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus("IN_TRANSIT");
|
||||
String before = order.getCurrentStatus();
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:OUTBOUNDED"); order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
@@ -1254,12 +1254,16 @@ public class WarehouseTransferService {
|
||||
if (sourceBalance == null || !customerId.equals(sourceBalance.getCustomerId())) {
|
||||
throw conflict("Source inventory balance is missing for warehouse transfer receipt");
|
||||
}
|
||||
if (sourceBalance.getInboundOrderId() == null || sourceBalance.getInboundOrderId() <= 0
|
||||
|| !StringUtils.hasText(sourceBalance.getInboundOrderNo()) || !StringUtils.hasText(sourceBalance.getInboundCode())) {
|
||||
throw conflict("Source inventory inbound identity is missing for warehouse transfer receipt");
|
||||
}
|
||||
WarehouseTransferReceiptCoordinator.Target target = context.target();
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, operatorId,
|
||||
new InventoryCommandRequests.StockIn(customerId, sourceBalance.getCustomerName(),
|
||||
target.warehouseId(), target.warehouseCode(), target.warehouseName(), target.fbaId(),
|
||||
target.fbaCode(), target.fbaName(),
|
||||
transfer.getLastlegOrderId(), transfer.getLastlegOrderNo(), transferNo,
|
||||
sourceBalance.getInboundOrderId(), sourceBalance.getInboundOrderNo(), sourceBalance.getInboundCode(),
|
||||
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : sourceBalance.getSkuCode(),
|
||||
sourceBalance.getProductNameZh(), sourceBalance.getProductNameEn(),
|
||||
target.palletId(), target.palletNo(),
|
||||
@@ -1281,7 +1285,7 @@ public class WarehouseTransferService {
|
||||
|| "RECEIVED".equals(value.getStatus()));
|
||||
transfer.setStatus(complete ? RECEIVED : PARTIALLY_RECEIVED); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus(complete ? "COMPLETED" : "IN_TRANSIT");
|
||||
String before = order.getCurrentStatus();
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + (complete ? RECEIVED : PARTIALLY_RECEIVED));
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
|
||||
Reference in New Issue
Block a user