fix(lastleg): complete stock transfer through linked relabel

This commit is contained in:
2026-10-04 20:21:16 +08:00
parent 7319fba35a
commit 76f02d8d45
17 changed files with 740 additions and 31 deletions
@@ -36,6 +36,8 @@ import java.util.Objects;
@Service
public class InventoryCommandService {
private record TransferMoveSnapshot(InventoryCommandRequests.Move move, String sku) {}
private record TransferAdjustmentSnapshot(InventoryCommandRequests.Adjustment adjustment, String sku) {}
@Resource private InventoryBalanceMapper balances;
@Resource private InventoryPositionMapper positions;
@Resource private InventoryLedgerMapper ledgers;
@@ -237,7 +239,35 @@ public class InventoryCommandService {
@Transactional
public InventoryCommandResponse ship(String customerId, String operatorId, InventoryCommandRequests.Move request) {
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> {
return ship(customerId, operatorId, request, null);
}
@Transactional
public InventoryCommandResponse allocateTransferSnapshot(String customerId, String operatorId,
InventoryCommandRequests.Move request, String skuSnapshot) {
requireTransferSku(request.idempotencyKey(), skuSnapshot);
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request, skuSnapshot);
}
@Transactional
public InventoryCommandResponse pickTransferSnapshot(String customerId, String operatorId,
InventoryCommandRequests.Move request, String skuSnapshot) {
requireTransferSku(request.idempotencyKey(), skuSnapshot);
return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request, skuSnapshot);
}
/** Exact source snapshot comes from the transfer's locked allocation, not a mixed order aggregate. */
@Transactional
public InventoryCommandResponse shipTransferSnapshot(String customerId, String operatorId,
InventoryCommandRequests.Move request, String skuSnapshot) {
requireTransferSku(request.idempotencyKey(), skuSnapshot);
return ship(customerId, operatorId, request, skuSnapshot);
}
private InventoryCommandResponse ship(String customerId, String operatorId,
InventoryCommandRequests.Move request, String skuSnapshot) {
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(),
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance();
@@ -255,7 +285,8 @@ public class InventoryCommandService {
balances.updateById(balance);
boolean palletEmpty = commands.palletPackages(position.getPalletId()) == 0;
if (palletEmpty) warehouseGateway.scrapPallet(customerId, position.getPalletId());
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), "SHIP",
ledger(customerId, operatorId, operation.getId(), balance, position,
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, "SHIP",
"PICKED", "SHIPPED", before, -request.packages(), before - request.packages(),
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
request.idempotencyKey(), request.remark());
@@ -471,10 +502,23 @@ public class InventoryCommandService {
@Transactional
public InventoryCommandResponse adjust(String customerId, String operatorId,
InventoryCommandRequests.Adjustment request) {
return adjust(customerId, operatorId, request, null);
}
@Transactional
public InventoryCommandResponse adjustTransferRelabel(String customerId, String operatorId,
InventoryCommandRequests.Adjustment request, String skuSnapshot) {
requireTransferSku(request.idempotencyKey(), skuSnapshot);
return adjust(customerId, operatorId, request, skuSnapshot);
}
private InventoryCommandResponse adjust(String customerId, String operatorId,
InventoryCommandRequests.Adjustment request, String skuSnapshot) {
if (request.packageDelta() == 0) {
throw badRequest("Adjustment package delta cannot be zero");
}
return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> {
return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(),
skuSnapshot == null ? request : new TransferAdjustmentSnapshot(request, skuSnapshot), operation -> {
Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance();
@@ -492,7 +536,8 @@ public class InventoryCommandService {
balance.setAvailablePackages(balanceAfter);
touch(balance);
balances.updateById(balance);
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), "ADJUST",
ledger(customerId, operatorId, operation.getId(), balance, position,
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, "ADJUST",
"AVAILABLE", "AVAILABLE", before, request.packageDelta(), after,
request.businessOrderNo(), null, null, null, request.idempotencyKey(),
request.reason() + remarkSuffix(request.remark()));
@@ -507,7 +552,25 @@ public class InventoryCommandService {
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
Bucket from, Bucket to, InventoryCommandRequests.Move request) {
return execute(customerId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
return move(customerId, operatorId, operationType, from, to, request, null);
}
@Transactional
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId,
InventoryCommandRequests.Move request, String skuSnapshot) {
requireTransferSku(request.idempotencyKey(), skuSnapshot);
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, skuSnapshot);
}
private static void requireTransferSku(String key, String skuSnapshot) {
if (key == null || !key.startsWith("WAREHOUSE_TRANSFER:") || skuSnapshot == null || skuSnapshot.isBlank()
|| "MIXED".equals(skuSnapshot)) throw badRequest("Exact warehouse transfer SKU snapshot is required");
}
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
Bucket from, Bucket to, InventoryCommandRequests.Move request, String skuSnapshot) {
return execute(customerId, operatorId, operationType, request.idempotencyKey(),
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
Locked locked = locked(customerId, request.positionId());
InventoryPosition position = locked.position();
InventoryBalance balance = locked.balance();
@@ -524,7 +587,8 @@ public class InventoryCommandService {
packages(balance, to, packages(balance, to) + request.packages());
touch(balance);
balances.updateById(balance);
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), operationType,
ledger(customerId, operatorId, operation.getId(), balance, position,
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, operationType,
from.name(), to.name(), beforeTarget, request.packages(), beforeTarget + request.packages(),
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
request.idempotencyKey(), request.remark());