fix(inventory): enforce warehouse transfer reservation ownership
This commit is contained in:
+9
@@ -12,6 +12,15 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InventoryCommandMapper {
|
||||
@Select("SELECT id FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND relabel_application_id=#{applicationId} FOR UPDATE")
|
||||
Long relabelTransfer(@Param("customerId") String customerId, @Param("applicationId") Long applicationId);
|
||||
|
||||
record TransferReservation(Long transferOrderId, long packageCount) {}
|
||||
|
||||
@Select("SELECT transfer_order_id,package_count FROM ll_warehouse_transfer_inventory_allocation " +
|
||||
"WHERE customer_id=#{customerId} AND inventory_position_id=#{positionId} AND status='RESERVED' FOR UPDATE")
|
||||
TransferReservation transferReservation(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE id=#{positionId} LIMIT 1")
|
||||
InventoryPosition findPosition(@Param("positionId") Long positionId);
|
||||
|
||||
|
||||
+62
-17
@@ -36,6 +36,7 @@ import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class InventoryCommandService {
|
||||
private record ReservedTransferMove(InventoryCommandRequests.Move move, String sku, Long transferOrderId) {}
|
||||
private record TransferMoveSnapshot(InventoryCommandRequests.Move move, String sku) {}
|
||||
private record TransferAdjustmentSnapshot(InventoryCommandRequests.Adjustment adjustment, String sku) {}
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@@ -243,32 +244,68 @@ public class InventoryCommandService {
|
||||
}
|
||||
|
||||
@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);
|
||||
public InventoryCommandResponse holdWarehouseTransfer(String customerId, String operatorId,
|
||||
Long transferOrderId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request, null,
|
||||
java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
@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);
|
||||
public InventoryCommandResponse releaseWarehouseTransfer(String customerId, String operatorId,
|
||||
Long transferOrderId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, null,
|
||||
java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
/** Exact source snapshot comes from the transfer's locked allocation, not a mixed order aggregate. */
|
||||
/** Internal transfer service entry points; no Controller binds this owner argument. */
|
||||
@Transactional
|
||||
public InventoryCommandResponse shipTransferSnapshot(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return ship(customerId, operatorId, request, skuSnapshot);
|
||||
public InventoryCommandResponse allocateWarehouseTransfer(String customerId, String operatorId,
|
||||
Long transferOrderId, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
if (skuSnapshot != null) requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED,
|
||||
request, skuSnapshot, java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse pickWarehouseTransfer(String customerId, String operatorId,
|
||||
Long transferOrderId, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
if (skuSnapshot != null) requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED,
|
||||
request, skuSnapshot, java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse shipWarehouseTransfer(String customerId, String operatorId,
|
||||
Long transferOrderId, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
if (skuSnapshot != null) requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return ship(customerId, operatorId, request, skuSnapshot, java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
private void requireReservation(String customerId, Long positionId, Long owner, long packages, String operationType) {
|
||||
var reservation = commands.transferReservation(customerId, positionId);
|
||||
if (reservation == null) {
|
||||
if (owner != null && !"RELEASE".equals(operationType) && !"HOLD".equals(operationType)) throw conflict("Warehouse transfer reservation is missing");
|
||||
return;
|
||||
}
|
||||
boolean partial = "RELEASE".equals(operationType) || "HOLD".equals(operationType);
|
||||
boolean quantityMatches = partial ? packages <= reservation.packageCount() : packages == reservation.packageCount();
|
||||
if (owner == null || !owner.equals(reservation.transferOrderId()) || !quantityMatches) {
|
||||
throw conflict("Inventory is reserved by a warehouse transfer");
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryCommandResponse ship(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
return ship(customerId, operatorId, request, skuSnapshot, null);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse ship(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot, Long reservationOwner) {
|
||||
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(),
|
||||
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
reservationOwner != null ? new ReservedTransferMove(request, skuSnapshot, reservationOwner)
|
||||
: skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
requireReservation(customerId, request.positionId(), reservationOwner, request.packages(), "SHIP");
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked");
|
||||
@@ -556,10 +593,11 @@ public class InventoryCommandService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId,
|
||||
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId, Long transferOrderId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, skuSnapshot);
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, skuSnapshot,
|
||||
java.util.Objects.requireNonNull(transferOrderId));
|
||||
}
|
||||
|
||||
private static void requireTransferSku(String key, String skuSnapshot) {
|
||||
@@ -569,9 +607,16 @@ public class InventoryCommandService {
|
||||
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
return move(customerId, operatorId, operationType, from, to, request, skuSnapshot, null);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request, String skuSnapshot, Long reservationOwner) {
|
||||
return execute(customerId, operatorId, operationType, request.idempotencyKey(),
|
||||
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
reservationOwner != null ? new ReservedTransferMove(request, skuSnapshot, reservationOwner)
|
||||
: skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
requireReservation(customerId, request.positionId(), reservationOwner, request.packages(), operationType);
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase());
|
||||
|
||||
Reference in New Issue
Block a user