fix(inventory): enforce warehouse transfer reservation ownership
This commit is contained in:
+5
-7
@@ -114,12 +114,10 @@ public interface DispatchCandidateMapper {
|
|||||||
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
||||||
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " +
|
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " +
|
||||||
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
|
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
|
||||||
// Warehouse-transfer inventory is intentionally reserved before dispatch. It must still be
|
// Incoming creation mode never grants ownership of a later transfer reservation.
|
||||||
// eligible for the normal smart-dispatch flow once its destination is bound; other order
|
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||||
// types keep the reservation guard to prevent duplicate dispatching.
|
|
||||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
|
||||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||||
// A warehouse-transfer pallet may contain both relabel and non-relabel inventory.
|
// A warehouse-transfer pallet may contain both relabel and non-relabel inventory.
|
||||||
// Do not hide the whole pallet because one carton/SKU has an active relabel item;
|
// Do not hide the whole pallet because one carton/SKU has an active relabel item;
|
||||||
// the remaining available inventory must still be dispatchable.
|
// the remaining available inventory must still be dispatchable.
|
||||||
@@ -213,9 +211,9 @@ public interface DispatchCandidateMapper {
|
|||||||
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
||||||
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||||
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
|
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
|
||||||
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
|
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
|
||||||
|
|||||||
+9
@@ -12,6 +12,15 @@ import java.util.List;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface InventoryCommandMapper {
|
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")
|
@Select("SELECT * FROM ll_inventory_position WHERE id=#{positionId} LIMIT 1")
|
||||||
InventoryPosition findPosition(@Param("positionId") Long positionId);
|
InventoryPosition findPosition(@Param("positionId") Long positionId);
|
||||||
|
|
||||||
|
|||||||
+62
-17
@@ -36,6 +36,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class InventoryCommandService {
|
public class InventoryCommandService {
|
||||||
|
private record ReservedTransferMove(InventoryCommandRequests.Move move, String sku, Long transferOrderId) {}
|
||||||
private record TransferMoveSnapshot(InventoryCommandRequests.Move move, String sku) {}
|
private record TransferMoveSnapshot(InventoryCommandRequests.Move move, String sku) {}
|
||||||
private record TransferAdjustmentSnapshot(InventoryCommandRequests.Adjustment adjustment, String sku) {}
|
private record TransferAdjustmentSnapshot(InventoryCommandRequests.Adjustment adjustment, String sku) {}
|
||||||
@Resource private InventoryBalanceMapper balances;
|
@Resource private InventoryBalanceMapper balances;
|
||||||
@@ -243,32 +244,68 @@ public class InventoryCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InventoryCommandResponse allocateTransferSnapshot(String customerId, String operatorId,
|
public InventoryCommandResponse holdWarehouseTransfer(String customerId, String operatorId,
|
||||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
Long transferOrderId, InventoryCommandRequests.Move request) {
|
||||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
return move(customerId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request, null,
|
||||||
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request, skuSnapshot);
|
java.util.Objects.requireNonNull(transferOrderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InventoryCommandResponse pickTransferSnapshot(String customerId, String operatorId,
|
public InventoryCommandResponse releaseWarehouseTransfer(String customerId, String operatorId,
|
||||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
Long transferOrderId, InventoryCommandRequests.Move request) {
|
||||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, null,
|
||||||
return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request, skuSnapshot);
|
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
|
@Transactional
|
||||||
public InventoryCommandResponse shipTransferSnapshot(String customerId, String operatorId,
|
public InventoryCommandResponse allocateWarehouseTransfer(String customerId, String operatorId,
|
||||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
Long transferOrderId, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
if (skuSnapshot != null) requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||||
return ship(customerId, operatorId, request, 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,
|
private InventoryCommandResponse ship(String customerId, String operatorId,
|
||||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
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(),
|
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());
|
Locked locked = locked(customerId, request.positionId());
|
||||||
|
requireReservation(customerId, request.positionId(), reservationOwner, request.packages(), "SHIP");
|
||||||
InventoryPosition position = locked.position();
|
InventoryPosition position = locked.position();
|
||||||
InventoryBalance balance = locked.balance();
|
InventoryBalance balance = locked.balance();
|
||||||
requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked");
|
requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked");
|
||||||
@@ -556,10 +593,11 @@ public class InventoryCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId,
|
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId, Long transferOrderId,
|
||||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||||
requireTransferSku(request.idempotencyKey(), 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) {
|
private static void requireTransferSku(String key, String skuSnapshot) {
|
||||||
@@ -569,9 +607,16 @@ public class InventoryCommandService {
|
|||||||
|
|
||||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||||
Bucket from, Bucket to, InventoryCommandRequests.Move request, String skuSnapshot) {
|
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(),
|
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());
|
Locked locked = locked(customerId, request.positionId());
|
||||||
|
requireReservation(customerId, request.positionId(), reservationOwner, request.packages(), operationType);
|
||||||
InventoryPosition position = locked.position();
|
InventoryPosition position = locked.position();
|
||||||
InventoryBalance balance = locked.balance();
|
InventoryBalance balance = locked.balance();
|
||||||
requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase());
|
requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase());
|
||||||
|
|||||||
+34
-8
@@ -58,6 +58,7 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class PdaPalletOperationService {
|
public class PdaPalletOperationService {
|
||||||
@Resource private PdaPalletOperationMapper mapper;
|
@Resource private PdaPalletOperationMapper mapper;
|
||||||
|
@Resource private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands;
|
||||||
@Resource private InventoryPositionMapper positions;
|
@Resource private InventoryPositionMapper positions;
|
||||||
@Resource private InventoryBalanceMapper balances;
|
@Resource private InventoryBalanceMapper balances;
|
||||||
@Resource private InventoryLedgerMapper ledgers;
|
@Resource private InventoryLedgerMapper ledgers;
|
||||||
@@ -465,14 +466,33 @@ public class PdaPalletOperationService {
|
|||||||
|
|
||||||
List<PalletPhysicalGateway.Transition> transitions = new ArrayList<>();
|
List<PalletPhysicalGateway.Transition> transitions = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
for (PalletChange change : plan.changes()) {
|
PalletOperationResponses.Operation result = transaction.execute(status -> {
|
||||||
transitions.add(physical.apply(identity.overseasWarehouseId(), change.pallet(), change.targetStatus(),
|
if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId());
|
||||||
change.targetLocationId(), transitionKey(identity.operatorKey(), normalizedKey,
|
Long relabelOwner = plan.relabelMutation() == null ? null : inventoryCommands.relabelTransfer(
|
||||||
change.pallet().palletId()), reason));
|
plan.relabelMutation().application().getCustomerId(), plan.relabelMutation().application().getId());
|
||||||
}
|
// Allocation also locks the position first: preserve mutual exclusion until commit.
|
||||||
saveTransitions(operation.getId(), transitions);
|
for (PlannedMove move : plan.moves()) {
|
||||||
PalletOperationResponses.Operation result = transaction.execute(status ->
|
PalletInventoryRow source = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()), move.positionId());
|
||||||
mutate(identity, operation, plan, normalizedKey, requestHash, reason));
|
if (source == null || !Objects.equals(source.getPalletId(), move.sourcePalletId())
|
||||||
|
|| value(source.getVersion()) != move.expectedVersion()) throw conflict("Inventory source changed");
|
||||||
|
requirePalletReservation(source.getCustomerId(), move.positionId(),
|
||||||
|
move.bucket() == InventoryBucket.HOLD ? relabelOwner : null);
|
||||||
|
}
|
||||||
|
if (plan.relabelMutation() != null) {
|
||||||
|
for (Long anchorId : plan.relabelMutation().anchors().keySet()) {
|
||||||
|
PalletInventoryRow anchor = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()), anchorId);
|
||||||
|
if (anchor == null) throw conflict("Relabel inventory anchor disappeared");
|
||||||
|
requirePalletReservation(anchor.getCustomerId(), anchorId, relabelOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (PalletChange change : plan.changes()) {
|
||||||
|
transitions.add(physical.apply(identity.overseasWarehouseId(), change.pallet(), change.targetStatus(),
|
||||||
|
change.targetLocationId(), transitionKey(identity.operatorKey(), normalizedKey,
|
||||||
|
change.pallet().palletId()), reason));
|
||||||
|
}
|
||||||
|
saveTransitions(operation.getId(), transitions);
|
||||||
|
return mutate(identity, operation, plan, normalizedKey, requestHash, reason);
|
||||||
|
});
|
||||||
if (result == null) throw new IllegalStateException("托盘作业事务没有返回结果");
|
if (result == null) throw new IllegalStateException("托盘作业事务没有返回结果");
|
||||||
return result;
|
return result;
|
||||||
} catch (RuntimeException failure) {
|
} catch (RuntimeException failure) {
|
||||||
@@ -482,6 +502,12 @@ public class PdaPalletOperationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requirePalletReservation(String customerId, Long positionId, Long owner) {
|
||||||
|
var reservation = inventoryCommands.transferReservation(customerId, positionId);
|
||||||
|
if (reservation != null && (owner == null || !owner.equals(reservation.transferOrderId())))
|
||||||
|
throw conflict("Inventory is reserved by a warehouse transfer");
|
||||||
|
}
|
||||||
|
|
||||||
private PalletOperationResponses.Operation mutate(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
private PalletOperationResponses.Operation mutate(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||||
Plan plan, String key, String requestHash, String reason) {
|
Plan plan, String key, String requestHash, String reason) {
|
||||||
if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId());
|
if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId());
|
||||||
|
|||||||
+34
-8
@@ -427,10 +427,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
int holdIndex = 0;
|
int holdIndex = 0;
|
||||||
for (SourceInventory source : sources) {
|
for (SourceInventory source : sources) {
|
||||||
if (source.returnCarton() != null) continue;
|
if (source.returnCarton() != null) continue;
|
||||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
holdRelabelInventory(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||||
source.request().sourceInventoryPositionId(), source.request().notifiedPackageCount(), relabelNo,
|
source.request().sourceInventoryPositionId(), source.request().notifiedPackageCount(), relabelNo,
|
||||||
null, "RELABEL:" + relabelNo, "RELABEL:" + applicationId + ":HOLD:" + holdIndex++,
|
null, "RELABEL:" + relabelNo, "RELABEL:" + applicationId + ":HOLD:" + holdIndex++,
|
||||||
"贴标申请冻结原入仓单库存"));
|
"贴标申请冻结原入仓单库存"), existingTailOrderId, null);
|
||||||
}
|
}
|
||||||
LastlegOrder tailOrder = existingTailOrderId == null
|
LastlegOrder tailOrder = existingTailOrderId == null
|
||||||
? insertTailOrder(customerId, request, requestHash, relabelNo, operationWarehouseId, total, now)
|
? insertTailOrder(customerId, request, requestHash, relabelNo, operationWarehouseId, total, now)
|
||||||
@@ -681,10 +681,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
for (RelabelItem item : itemList(application.getId())) {
|
for (RelabelItem item : itemList(application.getId())) {
|
||||||
if (item.getSourceInventoryPositionId() == null
|
if (item.getSourceInventoryPositionId() == null
|
||||||
|| Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) <= 0) continue;
|
|| Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) <= 0) continue;
|
||||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
holdRelabelInventory(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo,
|
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo,
|
||||||
null, "RELABEL:" + relabelNo, "RELABEL:" + application.getId() + ":RETRY_HOLD:"
|
null, "RELABEL:" + relabelNo, "RELABEL:" + application.getId() + ":RETRY_HOLD:"
|
||||||
+ walletVersion + ":" + holdIndex++, "补款后重试冻结换标库存"));
|
+ walletVersion + ":" + holdIndex++, "补款后重试冻结换标库存"), null, application);
|
||||||
}
|
}
|
||||||
String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER",
|
String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER",
|
||||||
relabelNo, application.getChargeAmount(), trim(request.remark()), walletVersion);
|
relabelNo, application.getChargeAmount(), trim(request.remark()), walletVersion);
|
||||||
@@ -739,10 +739,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
}
|
}
|
||||||
} else if (item.getSourceInventoryPositionId() != null
|
} else if (item.getSourceInventoryPositionId() != null
|
||||||
&& Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) > 0) {
|
&& Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) > 0) {
|
||||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
holdRelabelInventory(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo, null,
|
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo, null,
|
||||||
"RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++,
|
"RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++,
|
||||||
"换标驳回修改后重新冻结库存"));
|
"换标驳回修改后重新冻结库存"), null, application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
application.setLabelDeadline(request.labelDeadline());
|
application.setLabelDeadline(request.labelDeadline());
|
||||||
@@ -1214,13 +1214,39 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null) continue;
|
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null) continue;
|
||||||
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
|
releaseRelabelInventory(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), application.getRelabelNo(), null,
|
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), application.getRelabelNo(), null,
|
||||||
"RELABEL:" + application.getRelabelNo(), application.getIdempotencyKey() + ":RELEASE:"
|
"RELABEL:" + application.getRelabelNo(), application.getIdempotencyKey() + ":RELEASE:"
|
||||||
+ item.getSourceInventoryPositionId(), "贴标结束释放库存至原入仓单"));
|
+ item.getSourceInventoryPositionId(), "贴标结束释放库存至原入仓单"), application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WarehouseTransferOrder relabelInventoryOwner(String customerId, Long existingTailOrderId,
|
||||||
|
RelabelApplication application) {
|
||||||
|
if (application != null) {
|
||||||
|
WarehouseTransferOrder owner = warehouseTransfers.lockByRelabelNo(customerId, application.getRelabelNo());
|
||||||
|
if (owner != null && (!Objects.equals(owner.getRelabelApplicationId(), application.getId())
|
||||||
|
|| !Objects.equals(owner.getLastlegOrderId(), application.getLastlegOrderId())))
|
||||||
|
throw conflict("Relabel transfer association changed");
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
return existingTailOrderId == null ? null : warehouseTransfers.lockTailOrder(customerId, existingTailOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void holdRelabelInventory(String customerId, String operatorId, InventoryCommandRequests.Move request,
|
||||||
|
Long existingTailOrderId, RelabelApplication application) {
|
||||||
|
WarehouseTransferOrder owner = relabelInventoryOwner(customerId, existingTailOrderId, application);
|
||||||
|
if (owner == null) inventoryCommands.hold(customerId, operatorId, request);
|
||||||
|
else inventoryCommands.holdWarehouseTransfer(customerId, operatorId, owner.getId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseRelabelInventory(String customerId, String operatorId, InventoryCommandRequests.Move request,
|
||||||
|
RelabelApplication application) {
|
||||||
|
WarehouseTransferOrder owner = relabelInventoryOwner(customerId, null, application);
|
||||||
|
if (owner == null) inventoryCommands.release(customerId, operatorId, request);
|
||||||
|
else inventoryCommands.releaseWarehouseTransfer(customerId, operatorId, owner.getId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
private List<RelabelItem> itemList(Long applicationId) {
|
private List<RelabelItem> itemList(Long applicationId) {
|
||||||
return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId)
|
return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId)
|
||||||
.orderByAsc(RelabelItem::getId));
|
.orderByAsc(RelabelItem::getId));
|
||||||
|
|||||||
+2
@@ -6,6 +6,8 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
@Mapper public interface WarehouseTransferOrderMapper extends BaseMapper<WarehouseTransferOrder> {
|
@Mapper public interface WarehouseTransferOrderMapper extends BaseMapper<WarehouseTransferOrder> {
|
||||||
|
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND lastleg_order_id=#{tailOrderId} LIMIT 1 FOR UPDATE")
|
||||||
|
WarehouseTransferOrder lockTailOrder(@Param("customerId") String customerId, @Param("tailOrderId") Long tailOrderId);
|
||||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||||
WarehouseTransferOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
WarehouseTransferOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1")
|
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1")
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@ public class WarehouseTransferRelabelCompletionService {
|
|||||||
if (allocations.countActiveDispatchLock(customer, current.getPalletId()) > 0)
|
if (allocations.countActiveDispatchLock(customer, current.getPalletId()) > 0)
|
||||||
throw conflict("本票换标托盘已被排车占用");
|
throw conflict("本票换标托盘已被排车占用");
|
||||||
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL:" + item.getId();
|
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL:" + item.getId();
|
||||||
inventory.releaseTransferRelabel(customer, operator, new InventoryCommandRequests.Move(current.getId(), count,
|
inventory.releaseTransferRelabel(customer, operator, transfer.getId(), new InventoryCommandRequests.Move(current.getId(), count,
|
||||||
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票换标客户确认"), item.getSkuCode());
|
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票换标客户确认"), item.getSkuCode());
|
||||||
String outputSku = source.getNewSku() == null ? item.getSkuCode() : source.getNewSku();
|
String outputSku = source.getNewSku() == null ? item.getSkuCode() : source.getNewSku();
|
||||||
if (!outputSku.equals(item.getSkuCode())) {
|
if (!outputSku.equals(item.getSkuCode())) {
|
||||||
@@ -103,7 +103,7 @@ public class WarehouseTransferRelabelCompletionService {
|
|||||||
if (current == null || balance == null || current.getHoldPackages() == null || current.getHoldPackages() < count)
|
if (current == null || balance == null || current.getHoldPackages() == null || current.getHoldPackages() < count)
|
||||||
throw conflict("未换标的转运余量不可用");
|
throw conflict("未换标的转运余量不可用");
|
||||||
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":UNLABELLED:" + row.getId();
|
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":UNLABELLED:" + row.getId();
|
||||||
inventory.releaseTransferRelabel(customer, operator, new InventoryCommandRequests.Move(current.getId(), count,
|
inventory.releaseTransferRelabel(customer, operator, transfer.getId(), new InventoryCommandRequests.Move(current.getId(), count,
|
||||||
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票未换标余量确认"), row.getSkuCode());
|
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票未换标余量确认"), row.getSkuCode());
|
||||||
addTarget(targets, balance, current, count, row.getSkuCode());
|
addTarget(targets, balance, current, count, row.getSkuCode());
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-10
@@ -1122,7 +1122,7 @@ public class WarehouseTransferService {
|
|||||||
for (ConfirmedStockSource source : entry.getValue()) {
|
for (ConfirmedStockSource source : entry.getValue()) {
|
||||||
if (source.remainingPackages <= 0) continue;
|
if (source.remainingPackages <= 0) continue;
|
||||||
String holdKey = "WAREHOUSE_TRANSFER:" + transferNo + ":UNLABELLED:" + source.allocationId;
|
String holdKey = "WAREHOUSE_TRANSFER:" + transferNo + ":UNLABELLED:" + source.allocationId;
|
||||||
inventory.hold(customerId, operatorId, new InventoryCommandRequests.Move(source.positionId,
|
inventory.holdWarehouseTransfer(customerId, operatorId, transfer.getId(), new InventoryCommandRequests.Move(source.positionId,
|
||||||
source.remainingPackages, transferNo, null, holdKey, holdKey + ":HOLD", "本票转运未换标余量预留"));
|
source.remainingPackages, transferNo, null, holdKey, holdKey + ":HOLD", "本票转运未换标余量预留"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1181,21 +1181,18 @@ public class WarehouseTransferService {
|
|||||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(
|
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(
|
||||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark()));
|
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark()));
|
||||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
inventory.allocateWarehouseTransfer(customerId, operatorId, transfer.getId(), move,
|
||||||
inventory.allocateTransferSnapshot(customerId, operatorId, move, allocation.getSkuCode());
|
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null);
|
||||||
else inventory.allocate(customerId, operatorId, move);
|
|
||||||
var pick = new InventoryCommandRequests.Move(
|
var pick = new InventoryCommandRequests.Move(
|
||||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark()));
|
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark()));
|
||||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
inventory.pickWarehouseTransfer(customerId, operatorId, transfer.getId(), pick,
|
||||||
inventory.pickTransferSnapshot(customerId, operatorId, pick, allocation.getSkuCode());
|
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null);
|
||||||
else inventory.pick(customerId, operatorId, pick);
|
|
||||||
var ship = new InventoryCommandRequests.Move(
|
var ship = new InventoryCommandRequests.Move(
|
||||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark()));
|
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark()));
|
||||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
inventory.shipWarehouseTransfer(customerId, operatorId, transfer.getId(), ship,
|
||||||
inventory.shipTransferSnapshot(customerId, operatorId, ship, allocation.getSkuCode());
|
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null);
|
||||||
else inventory.ship(customerId, operatorId, ship);
|
|
||||||
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
|
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
|
||||||
allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation);
|
allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -14,6 +14,17 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
class DispatchCandidateMapperContractTest {
|
class DispatchCandidateMapperContractTest {
|
||||||
|
@Test
|
||||||
|
void historicalTransferCreationModeDoesNotGrantReservationOwnership() throws Exception {
|
||||||
|
for (Method method : List.of(
|
||||||
|
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class, String.class, String.class, String.class),
|
||||||
|
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
|
||||||
|
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||||
|
assertTrue(sql.contains("AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta"));
|
||||||
|
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void availableAndLockedPalletsApplyTheSameActiveRelabelBoundary() throws Exception {
|
void availableAndLockedPalletsApplyTheSameActiveRelabelBoundary() throws Exception {
|
||||||
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-relabel-boundary;MODE=MySQL")) {
|
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-relabel-boundary;MODE=MySQL")) {
|
||||||
|
|||||||
+37
@@ -74,6 +74,43 @@ class InventoryCommandServiceTest {
|
|||||||
lenient().when(commands.lockInboundTransitBalances(any(), any(), any())).thenReturn(List.of());
|
lenient().when(commands.lockInboundTransitBalances(any(), any(), any())).thenReturn(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsDispatchAndRelabelAgainstTransferReservationEvenWithForgedBusinessKey() {
|
||||||
|
var balance = balance(200L, 1);
|
||||||
|
var position = new InventoryPosition(); position.setId(300L); position.setBalanceId(200L);
|
||||||
|
position.setAvailablePackages(1L);
|
||||||
|
when(commands.lockPosition("100", 300L)).thenReturn(position);
|
||||||
|
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||||
|
lenient().when(commands.transferReservation("100", 300L))
|
||||||
|
.thenReturn(new InventoryCommandMapper.TransferReservation(900L, 1));
|
||||||
|
var request = new InventoryCommandRequests.Move(300L, 1, "WT-FORGED", "WT-FORGED",
|
||||||
|
"WAREHOUSE_TRANSFER_OUTBOUND:WT-FORGED", "WAREHOUSE_TRANSFER:WT-FORGED:ALLOCATE", null);
|
||||||
|
assertThrows(ResponseStatusException.class, () -> service.allocate("100", "7", request));
|
||||||
|
assertThrows(ResponseStatusException.class, () -> service.hold("100", "7", request));
|
||||||
|
assertEquals(1L, position.getAvailablePackages());
|
||||||
|
verify(positions, never()).updateById(any(InventoryPosition.class));
|
||||||
|
verify(ledgers, never()).insert(any(InventoryLedger.class));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
void onlyExactInternalTransferOwnerCanConsumeReservation() {
|
||||||
|
var balance = balance(200L, 1);
|
||||||
|
var position = position(300L, 200L, 1);
|
||||||
|
when(commands.lockPosition("100", 300L)).thenReturn(position);
|
||||||
|
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||||
|
when(commands.transferReservation("100", 300L)).thenReturn(new InventoryCommandMapper.TransferReservation(900L, 1));
|
||||||
|
var allocate = new InventoryCommandRequests.Move(300L, 1, "WT-1", "WT-1", "EVENT", "ALLOCATE-1", null);
|
||||||
|
assertThrows(ResponseStatusException.class, () -> service.allocateWarehouseTransfer("100", "7", 901L, allocate, null));
|
||||||
|
service.allocateWarehouseTransfer("100", "7", 900L, allocate, null);
|
||||||
|
var pick = new InventoryCommandRequests.Move(300L, 1, "WT-1", "WT-1", "EVENT", "PICK-1", null);
|
||||||
|
service.pickWarehouseTransfer("100", "7", 900L, pick, null);
|
||||||
|
var ship = new InventoryCommandRequests.Move(300L, 1, "WT-1", "WT-1", "EVENT", "SHIP-1", null);
|
||||||
|
service.shipWarehouseTransfer("100", "7", 900L, ship, null);
|
||||||
|
assertEquals(0L, position.getAvailablePackages());
|
||||||
|
assertEquals(0L, position.getPickedPackages());
|
||||||
|
verify(ledgers, times(3)).insert(any(InventoryLedger.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void stockInUpdatesBalancePositionAndLedgerInOneTransaction() throws Exception {
|
void stockInUpdatesBalancePositionAndLedgerInOneTransaction() throws Exception {
|
||||||
assertTrue(InventoryCommandService.class.getMethod("stockIn", String.class, String.class,
|
assertTrue(InventoryCommandService.class.getMethod("stockIn", String.class, String.class,
|
||||||
|
|||||||
+35
-1
@@ -59,6 +59,7 @@ class PdaPalletOperationServiceTest {
|
|||||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
|
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
|
||||||
}
|
}
|
||||||
@Mock private PdaPalletOperationMapper mapper;
|
@Mock private PdaPalletOperationMapper mapper;
|
||||||
|
@Mock private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands;
|
||||||
@Mock private InventoryPositionMapper positions;
|
@Mock private InventoryPositionMapper positions;
|
||||||
@Mock private InventoryBalanceMapper balances;
|
@Mock private InventoryBalanceMapper balances;
|
||||||
@Mock private InventoryLedgerMapper ledgers;
|
@Mock private InventoryLedgerMapper ledgers;
|
||||||
@@ -75,6 +76,7 @@ class PdaPalletOperationServiceTest {
|
|||||||
@BeforeEach void setUp() {
|
@BeforeEach void setUp() {
|
||||||
service = new PdaPalletOperationService();
|
service = new PdaPalletOperationService();
|
||||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||||
|
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
|
||||||
ReflectionTestUtils.setField(service, "positions", positions);
|
ReflectionTestUtils.setField(service, "positions", positions);
|
||||||
ReflectionTestUtils.setField(service, "balances", balances);
|
ReflectionTestUtils.setField(service, "balances", balances);
|
||||||
ReflectionTestUtils.setField(service, "ledgers", ledgers);
|
ReflectionTestUtils.setField(service, "ledgers", ledgers);
|
||||||
@@ -187,6 +189,35 @@ class PdaPalletOperationServiceTest {
|
|||||||
assertTrue(result.printableHtml().contains("RL-1"));
|
assertTrue(result.printableHtml().contains("RL-1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void reservedSourceIsRejectedBeforeAnyRemoteTransition() {
|
||||||
|
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L,
|
||||||
|
"OCCUPIED", null, 1);
|
||||||
|
var target = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L,
|
||||||
|
"IDLE", null, 2);
|
||||||
|
when(physical.resolve("SOURCE")).thenReturn(source);
|
||||||
|
when(physical.resolve("TARGET")).thenReturn(target);
|
||||||
|
when(physical.resolveLocation(88L, target.locationId())).thenReturn(
|
||||||
|
new PalletPhysicalGateway.LocationReference(target.locationId(), "LOC-1010", "STORAGE", "ACTIVE"));
|
||||||
|
var row = row(100L, 20L, 1L, 5L);
|
||||||
|
when(mapper.content("88", 20L)).thenReturn(List.of(row));
|
||||||
|
when(mapper.content("88", 10L)).thenReturn(List.of());
|
||||||
|
when(mapper.palletPackages("88", 20L)).thenReturn(5L);
|
||||||
|
when(mapper.palletPackages("88", 10L)).thenReturn(0L);
|
||||||
|
when(mapper.lockPosition("88", 100L)).thenReturn(row);
|
||||||
|
when(inventoryCommands.transferReservation(row.getCustomerId(), 100L)).thenReturn(
|
||||||
|
new com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper.TransferReservation(900L, 5));
|
||||||
|
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
|
||||||
|
doAnswer(call -> { operation.set(call.getArgument(0)); return 1; }).when(mapper).insert(any(PdaPalletOperation.class));
|
||||||
|
when(mapper.selectById(anyLong())).thenAnswer(call -> operation.get());
|
||||||
|
var request = new PalletOperationRequests.Assemble("reserved", new PalletOperationRequests.PalletExpectation("TARGET", 2),
|
||||||
|
List.of(new PalletOperationRequests.PalletExpectation("SOURCE", 1)),
|
||||||
|
List.of(new PalletOperationRequests.MoveLine(100L, 5L, 1L)), "assemble");
|
||||||
|
assertThrows(ResponseStatusException.class, () -> service.assemble(identity(), request));
|
||||||
|
verify(physical, never()).apply(anyLong(), any(), anyString(), anyLong(), anyString(), any());
|
||||||
|
verify(positions, never()).updateById(any(InventoryPosition.class));
|
||||||
|
verifyNoInteractions(ledgers);
|
||||||
|
}
|
||||||
|
|
||||||
@Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() {
|
@Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() {
|
||||||
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L,
|
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L,
|
||||||
"OCCUPIED", null, 1);
|
"OCCUPIED", null, 1);
|
||||||
@@ -203,7 +234,7 @@ class PdaPalletOperationServiceTest {
|
|||||||
when(mapper.content("88", 10L)).thenReturn(List.of());
|
when(mapper.content("88", 10L)).thenReturn(List.of());
|
||||||
when(mapper.palletPackages("88", 20L)).thenReturn(5L);
|
when(mapper.palletPackages("88", 20L)).thenReturn(5L);
|
||||||
when(mapper.palletPackages("88", 10L)).thenReturn(0L);
|
when(mapper.palletPackages("88", 10L)).thenReturn(0L);
|
||||||
when(mapper.lockPosition("88", 100L)).thenReturn(stale);
|
when(mapper.lockPosition("88", 100L)).thenReturn(planned, stale);
|
||||||
|
|
||||||
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
|
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
|
||||||
doAnswer(invocation -> { operation.set(invocation.getArgument(0)); return 1; })
|
doAnswer(invocation -> { operation.set(invocation.getArgument(0)); return 1; })
|
||||||
@@ -304,6 +335,9 @@ class PdaPalletOperationServiceTest {
|
|||||||
doAnswer(invocation -> { target.set(invocation.getArgument(0)); return 1; }).when(positions)
|
doAnswer(invocation -> { target.set(invocation.getArgument(0)); return 1; }).when(positions)
|
||||||
.insert(any(InventoryPosition.class));
|
.insert(any(InventoryPosition.class));
|
||||||
PalletInventoryRow held = row(100L, 20L, 3L, 0L); held.setPalletNo("SOURCE"); held.setHoldPackages(2L);
|
PalletInventoryRow held = row(100L, 20L, 3L, 0L); held.setPalletNo("SOURCE"); held.setHoldPackages(2L);
|
||||||
|
when(inventoryCommands.relabelTransfer(application.getCustomerId(), application.getId())).thenReturn(900L);
|
||||||
|
when(inventoryCommands.transferReservation(application.getCustomerId(), 100L)).thenReturn(
|
||||||
|
new com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper.TransferReservation(900L, 2));
|
||||||
when(mapper.content("88", 20L)).thenReturn(List.of(held));
|
when(mapper.content("88", 20L)).thenReturn(List.of(held));
|
||||||
when(mapper.content("88", 10L)).thenReturn(List.of());
|
when(mapper.content("88", 10L)).thenReturn(List.of());
|
||||||
when(mapper.lockPosition("88", 100L)).thenReturn(held);
|
when(mapper.lockPosition("88", 100L)).thenReturn(held);
|
||||||
|
|||||||
+1
-1
@@ -828,7 +828,7 @@ class RelabelApplicationServiceTest {
|
|||||||
assertEquals("SETTLE_PENDING", app.getWalletStatus()); assertEquals(new BigDecimal("0.01"), app.getChargeAmount());
|
assertEquals("SETTLE_PENDING", app.getWalletStatus()); assertEquals(new BigDecimal("0.01"), app.getChargeAmount());
|
||||||
assertEquals("CNY", app.getChargeCurrency());
|
assertEquals("CNY", app.getChargeCurrency());
|
||||||
verify(inventoryCommands, never()).release(any(), any(), any());
|
verify(inventoryCommands, never()).release(any(), any(), any());
|
||||||
verify(inventoryCommands, never()).releaseTransferRelabel(any(), any(), any(), any());
|
verify(inventoryCommands, never()).releaseTransferRelabel(any(), any(), any(), any(), any());
|
||||||
verify(transferRelabelCompletion, never()).complete(any(), any(), any(), any(), any());
|
verify(transferRelabelCompletion, never()).complete(any(), any(), any(), any(), any());
|
||||||
verify(histories, times(1)).insert(any(RelabelStatusHistory.class));
|
verify(histories, times(1)).insert(any(RelabelStatusHistory.class));
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-3
@@ -86,6 +86,50 @@ class WarehouseTransferRelabelPersistenceTest {
|
|||||||
session.getMapper(WarehouseTransferRelabelBindingMapper.class).insert(binding);
|
session.getMapper(WarehouseTransferRelabelBindingMapper.class).insert(binding);
|
||||||
}
|
}
|
||||||
void position(long id,long pallet,long available,long hold,String batch){jdbc.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,overseas_warehouse_id,destination_warehouse_code,pallet_id,pallet_no,batch_no,available_packages,hold_packages,allocated_packages,picked_packages,receiving_packages,version) VALUES (?,'7',60,'9','ABE4',?,?,?, ?,?,0,0,0,0)",id,pallet,"P-"+pallet,batch,available,hold);}
|
void position(long id,long pallet,long available,long hold,String batch){jdbc.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,overseas_warehouse_id,destination_warehouse_code,pallet_id,pallet_no,batch_no,available_packages,hold_packages,allocated_packages,picked_packages,receiving_packages,version) VALUES (?,'7',60,'9','ABE4',?,?,?, ?,?,0,0,0,0)",id,pallet,"P-"+pallet,batch,available,hold);}
|
||||||
|
@Test void linkedRelabelCreationAndReleaseUseRealTransferOwnerAndInventory() {
|
||||||
|
transfer.setRelabelNo("RL-OWN");
|
||||||
|
application.setRelabelNo("RL-OWN"); application.setLastlegOrderId(10L);
|
||||||
|
session.getMapper(WarehouseTransferOrderMapper.class).insert(transfer);
|
||||||
|
var relabel=new com.baiyue.lastleg.relabel.service.RelabelApplicationService();
|
||||||
|
ReflectionTestUtils.setField(relabel,"warehouseTransfers",session.getMapper(WarehouseTransferOrderMapper.class));
|
||||||
|
ReflectionTestUtils.setField(relabel,"inventoryCommands",inventory);
|
||||||
|
var hold=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(101L,1,"RL-OWN",null,
|
||||||
|
"RELABEL:RL-OWN","LINKED-HOLD",null);
|
||||||
|
tx.executeWithoutResult(status->ReflectionTestUtils.invokeMethod(relabel,"holdRelabelInventory",
|
||||||
|
"7","operator",hold,10L,null));
|
||||||
|
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101",Long.class));
|
||||||
|
var release=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(101L,1,"RL-OWN",null,
|
||||||
|
"RELABEL:RL-OWN","LINKED-RELEASE",null);
|
||||||
|
tx.executeWithoutResult(status->ReflectionTestUtils.invokeMethod(relabel,"releaseRelabelInventory",
|
||||||
|
"7","operator",release,application));
|
||||||
|
assertEquals(0L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101",Long.class));
|
||||||
|
var retry=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(101L,1,"RL-OWN",null,
|
||||||
|
"RELABEL:RL-OWN","LINKED-RETRY",null);
|
||||||
|
tx.executeWithoutResult(status->ReflectionTestUtils.invokeMethod(relabel,"holdRelabelInventory",
|
||||||
|
"7","operator",retry,null,application));
|
||||||
|
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101",Long.class));
|
||||||
|
var independent=new com.baiyue.lastleg.relabel.entity.RelabelApplication();
|
||||||
|
independent.setId(999L); independent.setLastlegOrderId(999L); independent.setRelabelNo("RL-OTHER");
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||||
|
()->tx.executeWithoutResult(status->ReflectionTestUtils.invokeMethod(relabel,"holdRelabelInventory",
|
||||||
|
"7","operator",new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(101L,1,
|
||||||
|
"RL-OTHER",null,"RELABEL:RL-OTHER","OTHER-HOLD",null),null,independent)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void realReservationBlocksNormalConsumersAndWrongInternalOwner() {
|
||||||
|
var request=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(101L,1,"WT-ORIGINAL","WT-ORIGINAL",
|
||||||
|
"WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:FORGED:ALLOCATE",null);
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||||
|
()->tx.executeWithoutResult(status->inventory.allocate("7","operator",request)));
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||||
|
()->tx.executeWithoutResult(status->inventory.hold("7","operator",request)));
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||||
|
()->tx.executeWithoutResult(status->inventory.allocateWarehouseTransfer("7","operator",2L,request,null)));
|
||||||
|
assertEquals(2L,jdbc.queryForObject("SELECT available_packages FROM ll_inventory_position WHERE id=101",Long.class));
|
||||||
|
assertEquals("RESERVED",jdbc.queryForObject("SELECT status FROM ll_warehouse_transfer_inventory_allocation WHERE id=13",String.class));
|
||||||
|
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test void confirmsMovedCartonIntoNewSkuAndPreservesOtherHoldWithRealCommandsAndMappers() {
|
@Test void confirmsMovedCartonIntoNewSkuAndPreservesOtherHoldWithRealCommandsAndMappers() {
|
||||||
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
||||||
assertEquals(4L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
assertEquals(4L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
||||||
@@ -121,12 +165,14 @@ class WarehouseTransferRelabelPersistenceTest {
|
|||||||
var allocate=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:ALLOCATE","");
|
var allocate=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:ALLOCATE","");
|
||||||
var pick=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:PICK","");
|
var pick=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:PICK","");
|
||||||
var ship=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:SHIP","");
|
var ship=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:SHIP","");
|
||||||
tx.executeWithoutResult(s->{inventory.allocateTransferSnapshot("7","operator",allocate,actual.getSkuCode());inventory.pickTransferSnapshot("7","operator",pick,actual.getSkuCode());inventory.shipTransferSnapshot("7","operator",ship,actual.getSkuCode());});
|
tx.executeWithoutResult(s->{inventory.allocateWarehouseTransfer("7","operator",1L,allocate,actual.getSkuCode());inventory.pickWarehouseTransfer("7","operator",1L,pick,actual.getSkuCode());inventory.shipWarehouseTransfer("7","operator",1L,ship,actual.getSkuCode());});
|
||||||
tx.executeWithoutResult(s->{assertTrue(inventory.allocateTransferSnapshot("7","operator",allocate,"NEW").replayed());assertTrue(inventory.pickTransferSnapshot("7","operator",pick,"NEW").replayed());assertTrue(inventory.shipTransferSnapshot("7","operator",ship,"NEW").replayed());});
|
tx.executeWithoutResult(s->{assertTrue(inventory.allocateWarehouseTransfer("7","operator",1L,allocate,"NEW").replayed());assertTrue(inventory.pickWarehouseTransfer("7","operator",1L,pick,"NEW").replayed());assertTrue(inventory.shipWarehouseTransfer("7","operator",1L,ship,"NEW").replayed());});
|
||||||
|
assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||||
|
()->tx.executeWithoutResult(status->inventory.allocateWarehouseTransfer("7","operator",2L,allocate,"NEW")));
|
||||||
assertEquals(3L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
assertEquals(3L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
||||||
assertEquals(1L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type='SHIP' AND sku_code='NEW'",Long.class));
|
assertEquals(1L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type='SHIP' AND sku_code='NEW'",Long.class));
|
||||||
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type IN ('SHIP','PICK','ALLOCATE') AND sku_code<>'NEW'",Long.class));
|
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type IN ('SHIP','PICK','ALLOCATE') AND sku_code<>'NEW'",Long.class));
|
||||||
assertThrows(RuntimeException.class,()->tx.executeWithoutResult(s->inventory.shipTransferSnapshot("7","operator",ship,"OLD")));
|
assertThrows(RuntimeException.class,()->tx.executeWithoutResult(s->inventory.shipWarehouseTransfer("7","operator",1L,ship,"OLD")));
|
||||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||||
}
|
}
|
||||||
@Test void cartonOnlyConfirmationKeepsSamePositionAndRetainsOriginalBindingHistory(){
|
@Test void cartonOnlyConfirmationKeepsSamePositionAndRetainsOriginalBindingHistory(){
|
||||||
|
|||||||
+5
-5
@@ -793,9 +793,9 @@ class WarehouseTransferServiceTest {
|
|||||||
var ordered = inOrder(inventory);
|
var ordered = inOrder(inventory);
|
||||||
ArgumentCaptor<com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move> move =
|
ArgumentCaptor<com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move> move =
|
||||||
ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move.class);
|
ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move.class);
|
||||||
ordered.verify(inventory).allocate(eq("TENANT-A"), eq("USER-1"), move.capture());
|
ordered.verify(inventory).allocateWarehouseTransfer(eq("TENANT-A"), eq("USER-1"), anyLong(), move.capture(), org.mockito.ArgumentMatchers.isNull());
|
||||||
ordered.verify(inventory).pick(eq("TENANT-A"), eq("USER-1"), move.capture());
|
ordered.verify(inventory).pickWarehouseTransfer(eq("TENANT-A"), eq("USER-1"), anyLong(), move.capture(), org.mockito.ArgumentMatchers.isNull());
|
||||||
ordered.verify(inventory).ship(eq("TENANT-A"), eq("USER-1"), move.capture());
|
ordered.verify(inventory).shipWarehouseTransfer(eq("TENANT-A"), eq("USER-1"), anyLong(), move.capture(), org.mockito.ArgumentMatchers.isNull());
|
||||||
assertTrue(move.getAllValues().stream().allMatch(value -> value.positionId() == 501L && value.packages() == 5));
|
assertTrue(move.getAllValues().stream().allMatch(value -> value.positionId() == 501L && value.packages() == 5));
|
||||||
assertEquals(WarehouseTransferService.OUTBOUNDED, transfer.getStatus());
|
assertEquals(WarehouseTransferService.OUTBOUNDED, transfer.getStatus());
|
||||||
assertEquals("SHIPPED", allocation.getStatus());
|
assertEquals("SHIPPED", allocation.getStatus());
|
||||||
@@ -958,7 +958,7 @@ class WarehouseTransferServiceTest {
|
|||||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||||
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
||||||
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
||||||
verify(inventory).shipTransferSnapshot(eq("TENANT-A"),eq("USER-1"),any(),eq("NEW"));
|
verify(inventory).shipWarehouseTransfer(eq("TENANT-A"),eq("USER-1"),anyLong(),any(),eq("NEW"));
|
||||||
verify(inventory,never()).ship(anyString(),anyString(),any());assertEquals("OUTBOUNDED",app.getFulfillmentStatus());
|
verify(inventory,never()).ship(anyString(),anyString(),any());assertEquals("OUTBOUNDED",app.getFulfillmentStatus());
|
||||||
var mixed=balance(601,101,"IN-1");mixed.setSkuCode("MIXED");when(inventoryCommands.lockBalance("TENANT-A",601L)).thenReturn(mixed);
|
var mixed=balance(601,101,"IN-1");mixed.setSkuCode("MIXED");when(inventoryCommands.lockBalance("TENANT-A",601L)).thenReturn(mixed);
|
||||||
when(inventory.stockIn(eq("TENANT-A"),eq("USER-1"),any())).thenReturn(new InventoryCommandResponse(1L,"STOCK_IN",801L,901L,5,0,0,0,false,false));
|
when(inventory.stockIn(eq("TENANT-A"),eq("USER-1"),any())).thenReturn(new InventoryCommandResponse(1L,"STOCK_IN",801L,901L,5,0,0,0,false,false));
|
||||||
@@ -984,7 +984,7 @@ class WarehouseTransferServiceTest {
|
|||||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||||
when(inventoryAllocations.lockTransfer("TENANT-A", 21L)).thenReturn(List.of(allocation));
|
when(inventoryAllocations.lockTransfer("TENANT-A", 21L)).thenReturn(List.of(allocation));
|
||||||
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "insufficient"))
|
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "insufficient"))
|
||||||
.when(inventory).pick(eq("TENANT-A"), eq("USER-1"), any());
|
.when(inventory).pickWarehouseTransfer(eq("TENANT-A"), eq("USER-1"), anyLong(), any(), org.mockito.ArgumentMatchers.isNull());
|
||||||
|
|
||||||
assertThrows(ResponseStatusException.class, () -> service.outbound("TENANT-A", "WTR-1", "USER-1",
|
assertThrows(ResponseStatusException.class, () -> service.outbound("TENANT-A", "WTR-1", "USER-1",
|
||||||
new WarehouseTransferRequests.Outbound("OUT-1", null)));
|
new WarehouseTransferRequests.Outbound("OUT-1", null)));
|
||||||
|
|||||||
Reference in New Issue
Block a user