diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java index 1f20c09b1..b676baa75 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java @@ -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('RECEIVED:',p.warehouse_pallet_id))) " + "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 - // eligible for the normal smart-dispatch flow once its destination is bound; other order - // 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 " + + // Incoming creation mode never grants ownership of a later transfer reservation. + "AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " + "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. // Do not hide the whole pallet because one carton/SKU has an active relabel item; // 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) " + "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)) " + - "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 " + - "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 " + "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 " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java index d52314fd8..ffe56f392 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java @@ -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); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java index 77e0528e5..d6c4cb571 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java @@ -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()); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java index 8fc1a31bb..1e4cf9624 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java @@ -58,6 +58,7 @@ import java.util.stream.Collectors; @Service public class PdaPalletOperationService { @Resource private PdaPalletOperationMapper mapper; + @Resource private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands; @Resource private InventoryPositionMapper positions; @Resource private InventoryBalanceMapper balances; @Resource private InventoryLedgerMapper ledgers; @@ -465,14 +466,33 @@ public class PdaPalletOperationService { List transitions = new ArrayList<>(); try { - 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); - PalletOperationResponses.Operation result = transaction.execute(status -> - mutate(identity, operation, plan, normalizedKey, requestHash, reason)); + PalletOperationResponses.Operation result = transaction.execute(status -> { + if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId()); + Long relabelOwner = plan.relabelMutation() == null ? null : inventoryCommands.relabelTransfer( + plan.relabelMutation().application().getCustomerId(), plan.relabelMutation().application().getId()); + // Allocation also locks the position first: preserve mutual exclusion until commit. + for (PlannedMove move : plan.moves()) { + PalletInventoryRow source = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()), move.positionId()); + 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("托盘作业事务没有返回结果"); return result; } 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, Plan plan, String key, String requestHash, String reason) { if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId()); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index dec53919b..796f0daf1 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -427,10 +427,10 @@ public class RelabelApplicationService implements WalletResultHandler { int holdIndex = 0; for (SourceInventory source : sources) { 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, null, "RELABEL:" + relabelNo, "RELABEL:" + applicationId + ":HOLD:" + holdIndex++, - "贴标申请冻结原入仓单库存")); + "贴标申请冻结原入仓单库存"), existingTailOrderId, null); } LastlegOrder tailOrder = existingTailOrderId == null ? insertTailOrder(customerId, request, requestHash, relabelNo, operationWarehouseId, total, now) @@ -681,10 +681,10 @@ public class RelabelApplicationService implements WalletResultHandler { for (RelabelItem item : itemList(application.getId())) { if (item.getSourceInventoryPositionId() == null || 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, null, "RELABEL:" + relabelNo, "RELABEL:" + application.getId() + ":RETRY_HOLD:" - + walletVersion + ":" + holdIndex++, "补款后重试冻结换标库存")); + + walletVersion + ":" + holdIndex++, "补款后重试冻结换标库存"), null, application); } String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER", relabelNo, application.getChargeAmount(), trim(request.remark()), walletVersion); @@ -739,10 +739,10 @@ public class RelabelApplicationService implements WalletResultHandler { } } else if (item.getSourceInventoryPositionId() != null && 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, "RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++, - "换标驳回修改后重新冻结库存")); + "换标驳回修改后重新冻结库存"), null, application); } } application.setLabelDeadline(request.labelDeadline()); @@ -1214,13 +1214,39 @@ public class RelabelApplicationService implements WalletResultHandler { 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, "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 itemList(Long applicationId) { return items.selectList(new LambdaQueryWrapper().eq(RelabelItem::getApplicationId, applicationId) .orderByAsc(RelabelItem::getId)); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java index bf0be19c6..86fae596e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java @@ -6,6 +6,8 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @Mapper public interface WarehouseTransferOrderMapper extends BaseMapper { + @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") 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") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java index 5b91f86af..013d8b638 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java @@ -71,7 +71,7 @@ public class WarehouseTransferRelabelCompletionService { if (allocations.countActiveDispatchLock(customer, current.getPalletId()) > 0) throw conflict("本票换标托盘已被排车占用"); 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()); String outputSku = source.getNewSku() == null ? item.getSkuCode() : source.getNewSku(); if (!outputSku.equals(item.getSkuCode())) { @@ -103,7 +103,7 @@ public class WarehouseTransferRelabelCompletionService { if (current == null || balance == null || current.getHoldPackages() == null || current.getHoldPackages() < count) throw conflict("未换标的转运余量不可用"); 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()); addTarget(targets, balance, current, count, row.getSkuCode()); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index dc4ec7b42..2765f0e2c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -1122,7 +1122,7 @@ public class WarehouseTransferService { for (ConfirmedStockSource source : entry.getValue()) { if (source.remainingPackages <= 0) continue; 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", "本票转运未换标余量预留")); } } @@ -1181,21 +1181,18 @@ public class WarehouseTransferService { InventoryCommandRequests.Move move = new InventoryCommandRequests.Move( allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo, "WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark())); - if (Boolean.TRUE.equals(transfer.getNeedRelabel())) - inventory.allocateTransferSnapshot(customerId, operatorId, move, allocation.getSkuCode()); - else inventory.allocate(customerId, operatorId, move); + inventory.allocateWarehouseTransfer(customerId, operatorId, transfer.getId(), move, + Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null); var pick = new InventoryCommandRequests.Move( allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo, "WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark())); - if (Boolean.TRUE.equals(transfer.getNeedRelabel())) - inventory.pickTransferSnapshot(customerId, operatorId, pick, allocation.getSkuCode()); - else inventory.pick(customerId, operatorId, pick); + inventory.pickWarehouseTransfer(customerId, operatorId, transfer.getId(), pick, + Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null); var ship = new InventoryCommandRequests.Move( allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo, "WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark())); - if (Boolean.TRUE.equals(transfer.getNeedRelabel())) - inventory.shipTransferSnapshot(customerId, operatorId, ship, allocation.getSkuCode()); - else inventory.ship(customerId, operatorId, ship); + inventory.shipWarehouseTransfer(customerId, operatorId, transfer.getId(), ship, + Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : null); allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null); allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java index 57c9f8b26..fbfa3203f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java @@ -14,6 +14,17 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; 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 void availableAndLockedPalletsApplyTheSameActiveRelabelBoundary() throws Exception { try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-relabel-boundary;MODE=MySQL")) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java index 89733caf6..163039013 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java @@ -74,6 +74,43 @@ class InventoryCommandServiceTest { 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 void stockInUpdatesBalancePositionAndLedgerInOneTransaction() throws Exception { assertTrue(InventoryCommandService.class.getMethod("stockIn", String.class, String.class, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java index 2541b2c2d..4f955fe2d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java @@ -59,6 +59,7 @@ class PdaPalletOperationServiceTest { assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app)); } @Mock private PdaPalletOperationMapper mapper; + @Mock private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands; @Mock private InventoryPositionMapper positions; @Mock private InventoryBalanceMapper balances; @Mock private InventoryLedgerMapper ledgers; @@ -75,6 +76,7 @@ class PdaPalletOperationServiceTest { @BeforeEach void setUp() { service = new PdaPalletOperationService(); ReflectionTestUtils.setField(service, "mapper", mapper); + ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands); ReflectionTestUtils.setField(service, "positions", positions); ReflectionTestUtils.setField(service, "balances", balances); ReflectionTestUtils.setField(service, "ledgers", ledgers); @@ -187,6 +189,35 @@ class PdaPalletOperationServiceTest { 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 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() { var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L, "OCCUPIED", null, 1); @@ -203,7 +234,7 @@ class PdaPalletOperationServiceTest { 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(stale); + when(mapper.lockPosition("88", 100L)).thenReturn(planned, stale); AtomicReference operation = new AtomicReference<>(); 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) .insert(any(InventoryPosition.class)); 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", 10L)).thenReturn(List.of()); when(mapper.lockPosition("88", 100L)).thenReturn(held); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index 5cf993e4a..93fac93d8 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -828,7 +828,7 @@ class RelabelApplicationServiceTest { assertEquals("SETTLE_PENDING", app.getWalletStatus()); assertEquals(new BigDecimal("0.01"), app.getChargeAmount()); assertEquals("CNY", app.getChargeCurrency()); 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(histories, times(1)).insert(any(RelabelStatusHistory.class)); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java index 900859a5f..7754cfa93 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java @@ -86,6 +86,50 @@ class WarehouseTransferRelabelPersistenceTest { 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);} + @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() { 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)); @@ -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 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",""); - 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->{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->{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.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(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)); - 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)); } @Test void cartonOnlyConfirmationKeepsSamePositionAndRetainsOriginalBindingHistory(){ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 2bb203921..a0015ff55 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -793,9 +793,9 @@ class WarehouseTransferServiceTest { var ordered = inOrder(inventory); ArgumentCaptor move = 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).pick(eq("TENANT-A"), eq("USER-1"), move.capture()); - ordered.verify(inventory).ship(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).pickWarehouseTransfer(eq("TENANT-A"), eq("USER-1"), anyLong(), move.capture(), org.mockito.ArgumentMatchers.isNull()); + 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)); assertEquals(WarehouseTransferService.OUTBOUNDED, transfer.getStatus()); assertEquals("SHIPPED", allocation.getStatus()); @@ -958,7 +958,7 @@ class WarehouseTransferServiceTest { 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")); - 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()); 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)); @@ -984,7 +984,7 @@ class WarehouseTransferServiceTest { when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); when(inventoryAllocations.lockTransfer("TENANT-A", 21L)).thenReturn(List.of(allocation)); 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", new WarehouseTransferRequests.Outbound("OUT-1", null)));