Merge origin/dev and resolve backend conflicts
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.inventory.command.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ReturnedInventoryRestoreRequest(
|
||||
String exceptionNo,
|
||||
String outboundNo,
|
||||
String overseasWarehouseId,
|
||||
Long inboundOrderId,
|
||||
List<SourceItem> sources,
|
||||
List<Receipt> receipts,
|
||||
String idempotencyKey,
|
||||
List<String> evidenceFileIds
|
||||
) {
|
||||
public record SourceItem(Long outboundPalletItemId, Long inventoryPositionId,
|
||||
String skuCode, long maxReturnPackages) { }
|
||||
|
||||
public record Receipt(Long outboundPalletItemId, Long inventoryPositionId, long receivedPackages,
|
||||
Long targetPalletId, String targetPalletNo,
|
||||
Long targetLocationId, String targetLocationCode) { }
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.inventory.command.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record ReturnedInventoryRestoreResponse(
|
||||
long adjustmentId,
|
||||
List<Long> ledgerIds,
|
||||
long beforePackages,
|
||||
long restoredPackages,
|
||||
long afterPackages,
|
||||
boolean replayed,
|
||||
LocalDateTime completedAt
|
||||
) {
|
||||
public ReturnedInventoryRestoreResponse asReplay() {
|
||||
return new ReturnedInventoryRestoreResponse(adjustmentId, ledgerIds, beforePackages,
|
||||
restoredPackages, afterPackages, true, completedAt);
|
||||
}
|
||||
}
|
||||
+28
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inventory.command.mapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@@ -32,6 +33,33 @@ public interface InventoryCommandMapper {
|
||||
@Param("inboundOrderId") Long inboundOrderId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND inbound_order_id=#{inboundOrderId} ORDER BY id FOR UPDATE")
|
||||
List<InventoryBalance> lockInboundBalances(@Param("customerId") String customerId,
|
||||
@Param("inboundOrderId") Long inboundOrderId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId}")
|
||||
long countPositions(@Param("customerId") String customerId, @Param("balanceId") Long balanceId);
|
||||
|
||||
@Delete("DELETE FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} " +
|
||||
"AND available_packages=0 AND hold_packages=0 AND allocated_packages=0 AND picked_packages=0 " +
|
||||
"AND in_transit_packages=0 AND receiving_packages=0 AND procurement_packages=0 " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id)")
|
||||
int deleteEmptyBalance(@Param("customerId") String customerId, @Param("balanceId") Long balanceId);
|
||||
|
||||
@Select("SELECT COALESCE(-SUM(delta_packages),0) FROM ll_inventory_ledger " +
|
||||
"WHERE customer_id=#{customerId} AND position_id=#{positionId} AND outbound_order_no=#{outboundNo} " +
|
||||
"AND sku_code=#{skuCode} AND business_type='SHIP' AND delta_packages<0")
|
||||
long shippedPackages(@Param("customerId") String customerId, @Param("positionId") Long positionId,
|
||||
@Param("outboundNo") String outboundNo, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(delta_packages),0) FROM ll_inventory_ledger " +
|
||||
"WHERE customer_id=#{customerId} AND source_event_id=#{sourceIdentity} " +
|
||||
"AND sku_code=#{skuCode} AND business_type='RETURN_STOCK_IN' AND delta_packages>0")
|
||||
long restoredPackages(@Param("customerId") String customerId,
|
||||
@Param("sourceIdentity") String sourceIdentity,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
|
||||
"WHERE p.customer_id=#{customerId} AND b.inbound_order_id=#{inboundOrderId} " +
|
||||
"AND p.hold_packages>0 ORDER BY p.id FOR UPDATE")
|
||||
|
||||
+302
-6
@@ -3,6 +3,8 @@ package com.baiyue.lastleg.inventory.command.service;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest;
|
||||
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse;
|
||||
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
|
||||
@@ -27,7 +29,10 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class InventoryCommandService {
|
||||
@@ -57,6 +62,44 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void rebindReceivingWarehouse(String customerId, Long inboundOrderId, long warehouseId,
|
||||
String warehouseCode, String warehouseName,
|
||||
String operatorId, String idempotencyKey) {
|
||||
String targetId = Long.toString(warehouseId);
|
||||
String targetCode = required(warehouseCode, "Receiving warehouse code is required");
|
||||
String targetName = required(warehouseName, "Receiving warehouse name is required");
|
||||
Long auditOperationId = IdWorker.getId();
|
||||
for (InventoryBalance balance : commands.lockInboundBalances(customerId, inboundOrderId)) {
|
||||
if (value(balance.getAvailablePackages()) != 0 || value(balance.getHoldPackages()) != 0
|
||||
|| value(balance.getAllocatedPackages()) != 0 || value(balance.getPickedPackages()) != 0
|
||||
|| value(balance.getReceivingPackages()) != 0 || value(balance.getProcurementPackages()) != 0
|
||||
|| commands.countPositions(customerId, balance.getId()) != 0) {
|
||||
throw conflict("Receiving warehouse cannot change after physical inventory exists");
|
||||
}
|
||||
InventoryBalance target = balances.lockDimension(customerId, targetId,
|
||||
balance.getDestinationWarehouseCode(), balance.getInboundOrderId(), balance.getInboundCode());
|
||||
if (target != null && !target.getId().equals(balance.getId())) {
|
||||
throw conflict("Receiving warehouse change would create a duplicate inventory dimension");
|
||||
}
|
||||
String sourceId = balance.getOverseasWarehouseId();
|
||||
if (Objects.equals(sourceId, targetId)
|
||||
&& Objects.equals(balance.getOverseasWarehouseCode(), targetCode)
|
||||
&& Objects.equals(balance.getOverseasWarehouseName(), targetName)) continue;
|
||||
long packages = value(balance.getInTransitPackages());
|
||||
balance.setOverseasWarehouseId(targetId);
|
||||
balance.setOverseasWarehouseCode(targetCode);
|
||||
balance.setOverseasWarehouseName(targetName);
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(customerId, operatorId, auditOperationId, balance, null, balance.getSkuCode(),
|
||||
"RECEIVING_WAREHOUSE_CHANGED", "IN_TRANSIT", "IN_TRANSIT",
|
||||
packages, 0, packages, null, balance.getInboundOrderNo(), null, null,
|
||||
idempotencyKey + ":BALANCE:" + balance.getId(),
|
||||
"In-transit receiving warehouse changed from " + sourceId + " to " + targetId);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse stockIn(String customerId, String operatorId,
|
||||
InventoryCommandRequests.StockIn request) {
|
||||
@@ -126,6 +169,9 @@ public class InventoryCommandService {
|
||||
transferred += moved;
|
||||
pending -= moved;
|
||||
if (targetDimension) continue;
|
||||
if (commands.countPositions(customerId, source.getId()) != 0) {
|
||||
throw conflict("In-transit source balance unexpectedly has physical positions");
|
||||
}
|
||||
touch(source);
|
||||
balances.updateById(source);
|
||||
ledger(customerId, operatorId, operationId, source, null, sku(request.skuCode()),
|
||||
@@ -133,6 +179,9 @@ public class InventoryCommandService {
|
||||
sourceBefore - moved, request.businessOrderNo(), request.inboundOrderNo(), null,
|
||||
request.sourceEventId(), request.idempotencyKey() + ":TRANSIT:" + source.getId(),
|
||||
"In-transit inventory transferred to overseas warehouse " + target.getOverseasWarehouseId());
|
||||
if (empty(source) && commands.deleteEmptyBalance(customerId, source.getId()) != 1) {
|
||||
throw conflict("Empty in-transit source balance changed concurrently");
|
||||
}
|
||||
}
|
||||
return transferred;
|
||||
}
|
||||
@@ -200,6 +249,175 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnedInventoryRestoreResponse restoreReturnedInventory(String customerId, String operatorId,
|
||||
ReturnedInventoryRestoreRequest request) {
|
||||
if (request == null) throw badRequest("Returned inventory restore request is required");
|
||||
String exceptionNo = required(request.exceptionNo(), "Exception number is required");
|
||||
String outboundNo = required(request.outboundNo(), "Outbound number is required");
|
||||
String warehouseId = required(request.overseasWarehouseId(), "Overseas warehouse ID is required");
|
||||
long warehouseResourceId;
|
||||
try {
|
||||
warehouseResourceId = Long.parseLong(warehouseId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw badRequest("Overseas warehouse ID must be numeric");
|
||||
}
|
||||
if (request.inboundOrderId() == null || request.inboundOrderId() <= 0) {
|
||||
throw badRequest("Inbound order ID is required");
|
||||
}
|
||||
if (request.sources() == null || request.sources().isEmpty()) {
|
||||
throw badRequest("Returned inventory sources are required");
|
||||
}
|
||||
if (request.receipts() == null || request.receipts().isEmpty()) {
|
||||
throw badRequest("Returned inventory receipts are required");
|
||||
}
|
||||
if (request.evidenceFileIds() == null || request.evidenceFileIds().isEmpty()
|
||||
|| request.evidenceFileIds().stream().anyMatch(value -> trim(value) == null)) {
|
||||
throw badRequest("Return evidence file IDs are required");
|
||||
}
|
||||
|
||||
Map<Long, ReturnedInventoryRestoreRequest.SourceItem> sources = new LinkedHashMap<>();
|
||||
for (ReturnedInventoryRestoreRequest.SourceItem source : request.sources()) {
|
||||
if (source == null || source.outboundPalletItemId() == null || source.outboundPalletItemId() <= 0
|
||||
|| source.inventoryPositionId() == null || source.inventoryPositionId() <= 0
|
||||
|| source.maxReturnPackages() <= 0) {
|
||||
throw badRequest("A valid returned inventory source is required");
|
||||
}
|
||||
String sourceSku = required(source.skuCode(), "Returned inventory source SKU is required");
|
||||
ReturnedInventoryRestoreRequest.SourceItem normalized = new ReturnedInventoryRestoreRequest.SourceItem(
|
||||
source.outboundPalletItemId(), source.inventoryPositionId(), sourceSku, source.maxReturnPackages());
|
||||
if (sources.putIfAbsent(source.outboundPalletItemId(), normalized) != null) {
|
||||
throw badRequest("Returned inventory source must be unique");
|
||||
}
|
||||
}
|
||||
Map<Long, Long> requestedBySource = new LinkedHashMap<>();
|
||||
Map<String, Boolean> receiptTargets = new LinkedHashMap<>();
|
||||
for (ReturnedInventoryRestoreRequest.Receipt receipt : request.receipts()) {
|
||||
ReturnedInventoryRestoreRequest.SourceItem source = receipt == null ? null
|
||||
: sources.get(receipt.outboundPalletItemId());
|
||||
if (source == null || !Objects.equals(source.inventoryPositionId(), receipt.inventoryPositionId())) {
|
||||
throw badRequest("Return receipt must match an immutable outbound inventory source");
|
||||
}
|
||||
if (receipt.receivedPackages() <= 0 || receipt.targetPalletId() == null || receipt.targetPalletId() <= 0
|
||||
|| receipt.targetLocationId() == null || receipt.targetLocationId() <= 0) {
|
||||
throw badRequest("A valid returned inventory receipt is required");
|
||||
}
|
||||
required(receipt.targetPalletNo(), "Target pallet number is required");
|
||||
required(receipt.targetLocationCode(), "Target location code is required");
|
||||
String receiptTarget = receipt.outboundPalletItemId() + ":" + receipt.targetPalletId();
|
||||
if (receiptTargets.putIfAbsent(receiptTarget, Boolean.TRUE) != null) {
|
||||
throw badRequest("Return receipt source and target pallet must be unique");
|
||||
}
|
||||
requestedBySource.merge(source.outboundPalletItemId(), receipt.receivedPackages(), Long::sum);
|
||||
}
|
||||
for (ReturnedInventoryRestoreRequest.SourceItem source : sources.values()) {
|
||||
long requested = requestedBySource.getOrDefault(source.outboundPalletItemId(), 0L);
|
||||
if (requested <= 0 || requested > source.maxReturnPackages()) {
|
||||
throw conflict("Returned packages exceed the approved source limit");
|
||||
}
|
||||
}
|
||||
|
||||
String normalizedKey = required(request.idempotencyKey(), "Idempotency key is required");
|
||||
String requestHash = hash(request);
|
||||
InventoryOperation existing = operations.findIdempotency(customerId, normalizedKey);
|
||||
if (existing != null) return replayReturned(existing, requestHash);
|
||||
|
||||
InventoryOperation operation = new InventoryOperation();
|
||||
operation.setId(IdWorker.getId());
|
||||
operation.setCustomerId(customerId);
|
||||
operation.setIdempotencyKey(normalizedKey);
|
||||
operation.setOperationType("RETURN_STOCK_IN");
|
||||
operation.setRequestHash(requestHash);
|
||||
operation.setStatus("PROCESSING");
|
||||
operation.setCreatedAt(LocalDateTime.now());
|
||||
operation.setUpdatedAt(operation.getCreatedAt());
|
||||
try {
|
||||
operations.insert(operation);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryOperation concurrent = operations.findIdempotency(customerId, normalizedKey);
|
||||
if (concurrent != null) return replayReturned(concurrent, requestHash);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
Map<Long, Locked> lockedSources = new LinkedHashMap<>();
|
||||
Map<Long, Long> balanceBefore = new LinkedHashMap<>();
|
||||
for (ReturnedInventoryRestoreRequest.SourceItem source : sources.values()) {
|
||||
Locked locked = locked(customerId, source.inventoryPositionId());
|
||||
InventoryBalance balance = locked.balance();
|
||||
if (!Objects.equals(balance.getInboundOrderId(), request.inboundOrderId())) {
|
||||
throw conflict("Returned inventory source inbound order does not match");
|
||||
}
|
||||
if (!warehouseId.equals(trim(balance.getOverseasWarehouseId()))) {
|
||||
throw conflict("Returned inventory source warehouse does not match");
|
||||
}
|
||||
long shipped = commands.shippedPackages(customerId, source.inventoryPositionId(), outboundNo,
|
||||
source.skuCode());
|
||||
long restored = commands.restoredPackages(customerId, returnSourceIdentity(source), source.skuCode());
|
||||
long requested = requestedBySource.get(source.outboundPalletItemId());
|
||||
if (shipped <= 0) throw conflict("Immutable outbound shipment source was not found");
|
||||
if (restored + requested > shipped) {
|
||||
throw conflict("Returned packages exceed the remaining shipped packages");
|
||||
}
|
||||
lockedSources.put(source.outboundPalletItemId(), locked);
|
||||
balanceBefore.putIfAbsent(balance.getId(), value(balance.getAvailablePackages()));
|
||||
}
|
||||
|
||||
long restoredPackages = 0;
|
||||
List<Long> ledgerIds = new java.util.ArrayList<>();
|
||||
for (ReturnedInventoryRestoreRequest.Receipt receipt : request.receipts()) {
|
||||
ReturnedInventoryRestoreRequest.SourceItem source = sources.get(receipt.outboundPalletItemId());
|
||||
Locked sourceLocked = lockedSources.get(receipt.outboundPalletItemId());
|
||||
InventoryBalance balance = sourceLocked.balance();
|
||||
WarehouseResourceGateway.WarehousePalletReference pallet = warehouseGateway.resolvePallet(
|
||||
receipt.targetPalletId());
|
||||
if (pallet == null || pallet.palletId() != receipt.targetPalletId()
|
||||
|| pallet.warehouseId() != warehouseResourceId
|
||||
|| pallet.locationId() != receipt.targetLocationId()
|
||||
|| !required(receipt.targetPalletNo(), "Target pallet number is required").equals(pallet.palletNo())) {
|
||||
throw conflict("Target pallet does not match the returned inventory warehouse and location");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseLocationReference location = warehouseGateway.resolveLocation(
|
||||
warehouseResourceId, receipt.targetLocationId());
|
||||
if (location == null || location.locationId() != receipt.targetLocationId()
|
||||
|| !required(receipt.targetLocationCode(), "Target location code is required")
|
||||
.equals(location.locationCode())) {
|
||||
throw conflict("Target location does not match the returned inventory warehouse");
|
||||
}
|
||||
|
||||
InventoryPosition target = returnedPosition(customerId, balance, exceptionNo, receipt, location);
|
||||
long before = value(target.getAvailablePackages());
|
||||
target.setAvailablePackages(before + receipt.receivedPackages());
|
||||
target.setStatus("OCCUPIED");
|
||||
target.setVersion(value(target.getVersion()) + 1);
|
||||
target.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(target);
|
||||
|
||||
balance.setAvailablePackages(value(balance.getAvailablePackages()) + receipt.receivedPackages());
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
InventoryLedger ledger = ledger(customerId, operatorId, operation.getId(), balance, target,
|
||||
source.skuCode(), "RETURN_STOCK_IN", "SHIPPED", "AVAILABLE", before,
|
||||
receipt.receivedPackages(), before + receipt.receivedPackages(), exceptionNo,
|
||||
balance.getInboundOrderNo(), outboundNo, returnSourceIdentity(source),
|
||||
normalizedKey + ":" + receipt.outboundPalletItemId() + ":" + target.getId(),
|
||||
"Exception return received with evidence " + request.evidenceFileIds().size());
|
||||
ledgerIds.add(ledger.getId());
|
||||
restoredPackages += receipt.receivedPackages();
|
||||
}
|
||||
long beforePackages = balanceBefore.values().stream().mapToLong(Long::longValue).sum();
|
||||
long afterPackages = balanceBefore.keySet().stream()
|
||||
.mapToLong(balanceId -> value(lockedSources.values().stream()
|
||||
.map(Locked::balance).filter(balance -> balance.getId().equals(balanceId)).findFirst().orElseThrow()
|
||||
.getAvailablePackages())).sum();
|
||||
ReturnedInventoryRestoreResponse response = new ReturnedInventoryRestoreResponse(operation.getId(),
|
||||
List.copyOf(ledgerIds), beforePackages, restoredPackages, afterPackages, false, LocalDateTime.now());
|
||||
operation.setStatus("COMPLETED");
|
||||
operation.setResultJson(write(response));
|
||||
operation.setUpdatedAt(response.completedAt());
|
||||
operations.updateById(operation);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjust(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request) {
|
||||
@@ -287,6 +505,9 @@ public class InventoryCommandService {
|
||||
String inboundCode, String requestedSkuCode, String productNameZh,
|
||||
String productNameEn) {
|
||||
String frozenCustomerName = required(customerName, "Customer name snapshot is required");
|
||||
String frozenWarehouseId = required(overseasWarehouseId, "Overseas warehouse ID snapshot is required");
|
||||
String frozenWarehouseCode = required(overseasWarehouseCode, "Overseas warehouse code snapshot is required");
|
||||
String frozenWarehouseName = required(overseasWarehouseName, "Overseas warehouse name snapshot is required");
|
||||
String skuCode = sku(requestedSkuCode);
|
||||
String destinationId = trim(destinationWarehouseId);
|
||||
String destinationCode = trim(destinationWarehouseCode);
|
||||
@@ -294,10 +515,11 @@ public class InventoryCommandService {
|
||||
throw badRequest("Destination warehouse ID or code is required");
|
||||
}
|
||||
if (destinationCode == null) destinationCode = "";
|
||||
InventoryBalance balance = balances.lockDimension(customerId, overseasWarehouseId,
|
||||
InventoryBalance balance = balances.lockDimension(customerId, frozenWarehouseId,
|
||||
destinationCode, inboundOrderId, inboundCode);
|
||||
if (balance != null) {
|
||||
requireCustomerSnapshot(balance, frozenCustomerName);
|
||||
requireWarehouseSnapshot(balance, frozenWarehouseId, frozenWarehouseCode, frozenWarehouseName);
|
||||
mergeSkuSnapshot(balance, skuCode);
|
||||
return balance;
|
||||
}
|
||||
@@ -305,9 +527,9 @@ public class InventoryCommandService {
|
||||
balance.setId(IdWorker.getId());
|
||||
balance.setCustomerId(customerId);
|
||||
balance.setCustomerName(frozenCustomerName);
|
||||
balance.setOverseasWarehouseId(overseasWarehouseId.trim());
|
||||
balance.setOverseasWarehouseCode(trim(overseasWarehouseCode));
|
||||
balance.setOverseasWarehouseName(trim(overseasWarehouseName));
|
||||
balance.setOverseasWarehouseId(frozenWarehouseId);
|
||||
balance.setOverseasWarehouseCode(frozenWarehouseCode);
|
||||
balance.setOverseasWarehouseName(frozenWarehouseName);
|
||||
balance.setDestinationWarehouseId(destinationId);
|
||||
balance.setDestinationWarehouseCode(destinationCode);
|
||||
balance.setDestinationWarehouseName(trim(destinationWarehouseName));
|
||||
@@ -325,10 +547,11 @@ public class InventoryCommandService {
|
||||
balances.insert(balance);
|
||||
return balance;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryBalance concurrent = balances.lockDimension(customerId, overseasWarehouseId,
|
||||
InventoryBalance concurrent = balances.lockDimension(customerId, frozenWarehouseId,
|
||||
destinationCode, inboundOrderId, inboundCode);
|
||||
if (concurrent != null) {
|
||||
requireCustomerSnapshot(concurrent, frozenCustomerName);
|
||||
requireWarehouseSnapshot(concurrent, frozenWarehouseId, frozenWarehouseCode, frozenWarehouseName);
|
||||
mergeSkuSnapshot(concurrent, skuCode);
|
||||
return concurrent;
|
||||
}
|
||||
@@ -433,7 +656,63 @@ public class InventoryCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
private void ledger(String customerId, String operatorId, Long operationId, InventoryBalance balance,
|
||||
private InventoryPosition returnedPosition(String customerId, InventoryBalance balance, String exceptionNo,
|
||||
ReturnedInventoryRestoreRequest.Receipt receipt,
|
||||
WarehouseResourceGateway.WarehouseLocationReference location) {
|
||||
String batchNo = "RETURN:" + exceptionNo;
|
||||
InventoryPosition position = positions.lockPosition(customerId, balance.getId(), receipt.targetPalletId(), batchNo);
|
||||
if (position != null) {
|
||||
if (!Objects.equals(position.getWarehouseLocationId(), location.locationId())
|
||||
|| !Objects.equals(position.getWarehouseLocationCode(), location.locationCode())
|
||||
|| !Objects.equals(position.getPalletNo(), receipt.targetPalletNo())) {
|
||||
throw conflict("Returned inventory target position snapshot does not match");
|
||||
}
|
||||
return position;
|
||||
}
|
||||
position = new InventoryPosition();
|
||||
position.setId(IdWorker.getId());
|
||||
position.setCustomerId(customerId);
|
||||
position.setBalanceId(balance.getId());
|
||||
position.setOverseasWarehouseId(balance.getOverseasWarehouseId());
|
||||
position.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
|
||||
position.setPalletId(receipt.targetPalletId());
|
||||
position.setPalletNo(receipt.targetPalletNo().trim());
|
||||
position.setBatchNo(batchNo);
|
||||
position.setWarehouseLocationId(location.locationId());
|
||||
position.setWarehouseLocationCode(location.locationCode());
|
||||
position.setAvailablePackages(0L);
|
||||
position.setHoldPackages(0L);
|
||||
position.setAllocatedPackages(0L);
|
||||
position.setPickedPackages(0L);
|
||||
position.setReceivingPackages(0L);
|
||||
position.setStatus("OCCUPIED");
|
||||
position.setVersion(0L);
|
||||
position.setCreatedAt(LocalDateTime.now());
|
||||
position.setUpdatedAt(position.getCreatedAt());
|
||||
positions.insert(position);
|
||||
return position;
|
||||
}
|
||||
|
||||
private ReturnedInventoryRestoreResponse replayReturned(InventoryOperation operation, String requestHash) {
|
||||
if (!"RETURN_STOCK_IN".equals(operation.getOperationType())
|
||||
|| !requestHash.equals(operation.getRequestHash())) {
|
||||
throw conflict("Idempotency key was already used with a different inventory request");
|
||||
}
|
||||
if (!"COMPLETED".equals(operation.getStatus()) || trim(operation.getResultJson()) == null) {
|
||||
throw conflict("Inventory operation is still processing");
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(operation.getResultJson(), ReturnedInventoryRestoreResponse.class).asReplay();
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new IllegalStateException("Cannot read returned inventory idempotency result", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static String returnSourceIdentity(ReturnedInventoryRestoreRequest.SourceItem source) {
|
||||
return "RETURN_SOURCE:" + source.outboundPalletItemId() + ":" + source.inventoryPositionId();
|
||||
}
|
||||
|
||||
private InventoryLedger ledger(String customerId, String operatorId, Long operationId, InventoryBalance balance,
|
||||
InventoryPosition position, String skuCode, String businessType, String from, String to,
|
||||
long beforePackages, long deltaPackages, long afterPackages,
|
||||
String businessOrderNo, String inboundOrderNo, String outboundOrderNo,
|
||||
@@ -474,6 +753,7 @@ public class InventoryCommandService {
|
||||
ledger.setOccurredAt(LocalDateTime.now());
|
||||
ledger.setCreatedAt(ledger.getOccurredAt());
|
||||
ledgers.insert(ledger);
|
||||
return ledger;
|
||||
}
|
||||
|
||||
private static void mergeSkuSnapshot(InventoryBalance balance, String skuCode) {
|
||||
@@ -505,6 +785,13 @@ public class InventoryCommandService {
|
||||
balance.setProcurementPackages(0L);
|
||||
}
|
||||
|
||||
private static boolean empty(InventoryBalance balance) {
|
||||
return value(balance.getAvailablePackages()) == 0 && value(balance.getHoldPackages()) == 0
|
||||
&& value(balance.getAllocatedPackages()) == 0 && value(balance.getPickedPackages()) == 0
|
||||
&& value(balance.getInTransitPackages()) == 0 && value(balance.getReceivingPackages()) == 0
|
||||
&& value(balance.getProcurementPackages()) == 0;
|
||||
}
|
||||
|
||||
private static long positionPackages(InventoryPosition position) {
|
||||
return value(position.getAvailablePackages()) + value(position.getHoldPackages())
|
||||
+ value(position.getAllocatedPackages()) + value(position.getPickedPackages())
|
||||
@@ -583,6 +870,15 @@ public class InventoryCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireWarehouseSnapshot(InventoryBalance balance, String requestedWarehouseId,
|
||||
String requestedWarehouseCode, String requestedWarehouseName) {
|
||||
if (!requestedWarehouseId.equals(trim(balance.getOverseasWarehouseId()))
|
||||
|| !requestedWarehouseCode.equals(trim(balance.getOverseasWarehouseCode()))
|
||||
|| !requestedWarehouseName.equals(trim(balance.getOverseasWarehouseName()))) {
|
||||
throw conflict("Inventory balance overseas warehouse snapshot does not match");
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(String value, String message) {
|
||||
String normalized = trim(value);
|
||||
if (normalized == null) throw badRequest(message);
|
||||
|
||||
+3
-1
@@ -35,8 +35,10 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Update("UPDATE ll_inventory_balance SET inbound_order_no=#{planNo},updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode}")
|
||||
"WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{overseasWarehouseId} " +
|
||||
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode}")
|
||||
int bindInboundPlan(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId,
|
||||
@Param("inboundOrderId") Long inboundOrderId,
|
||||
@Param("inboundCode") String inboundCode,
|
||||
@Param("planNo") String planNo,
|
||||
|
||||
+13
-3
@@ -38,7 +38,7 @@ public class InventoryQueryController {
|
||||
|
||||
@GetMapping(BASE_PATH + "/summary")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
|
||||
@Operation(summary = "Get inbound-order package inventory totals", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
@Operation(summary = "Get inbound-order package inventory totals", description = "Zero-balance dimensions are excluded. Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<InventoryDashboardResponse> summary(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@@ -48,7 +48,7 @@ public class InventoryQueryController {
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
|
||||
@Operation(summary = "Page inventory by customer, warehouse and inbound order", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
@Operation(summary = "Page inventory by customer, warehouse and inbound order", description = "Returns only non-zero dimensions with the frozen receiving overseas warehouse ID/code/name. Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<PageResult<InventoryBalanceView>> balances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@@ -56,6 +56,16 @@ public class InventoryQueryController {
|
||||
return ApiResponse.success(service.balances(platformScope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances/{balanceId}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
|
||||
@Operation(summary = "Get one inventory balance", description = "Returns one non-zero inventory balance by ID and optional customer scope. Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<InventoryBalanceView> balance(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long balanceId,
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.balance(platformScope(user, customerId), balanceId));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_LIST + "')")
|
||||
@Operation(summary = "List pallet and batch positions for one inbound-order balance", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
@@ -79,7 +89,7 @@ public class InventoryQueryController {
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EXPORT + "')")
|
||||
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Uses the same non-zero balance filter and frozen receiving warehouse snapshots as the page API. Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
public ResponseEntity<byte[]> exportBalances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
|
||||
+15
-2
@@ -14,7 +14,8 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InventoryQueryMapper {
|
||||
String BALANCE_FILTER = " WHERE 1=1" +
|
||||
String BALANCE_FILTER = " WHERE (available_packages+hold_packages+allocated_packages+picked_packages+" +
|
||||
"in_transit_packages+receiving_packages+procurement_packages)>0" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND customer_id=#{customerId}</if>" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
@@ -37,7 +38,8 @@ public interface InventoryQueryMapper {
|
||||
"<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND procurement_packages > 0</when>" +
|
||||
"</choose>";
|
||||
|
||||
String JOINED_BALANCE_FILTER = " WHERE 1=1" +
|
||||
String JOINED_BALANCE_FILTER = " WHERE (b.available_packages+b.hold_packages+b.allocated_packages+" +
|
||||
"b.picked_packages+b.in_transit_packages+b.receiving_packages+b.procurement_packages)>0" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND b.customer_id=#{customerId}</if>" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND b.customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND b.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
@@ -109,6 +111,17 @@ public interface InventoryQueryMapper {
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE id=#{balanceId} LIMIT 1")
|
||||
InventoryBalance findBalance(@Param("balanceId") long balanceId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} " +
|
||||
"AND (available_packages+hold_packages+allocated_packages+picked_packages+in_transit_packages+" +
|
||||
"receiving_packages+procurement_packages)>0 LIMIT 1")
|
||||
InventoryBalance ownedVisibleBalance(@Param("customerId") String customerId,
|
||||
@Param("balanceId") long balanceId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE id=#{balanceId} " +
|
||||
"AND (available_packages+hold_packages+allocated_packages+picked_packages+in_transit_packages+" +
|
||||
"receiving_packages+procurement_packages)>0 LIMIT 1")
|
||||
InventoryBalance visibleBalance(@Param("balanceId") long balanceId);
|
||||
|
||||
String LEDGER_FILTER = " WHERE 1=1" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND customer_id=#{customerId}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
|
||||
+10
@@ -57,6 +57,16 @@ public class InventoryQueryService {
|
||||
return page(records, query.page(), query.size(), total);
|
||||
}
|
||||
|
||||
public InventoryBalanceView balance(String customerId, long balanceId) {
|
||||
InventoryBalance balance = customerId == null
|
||||
? mapper.visibleBalance(balanceId)
|
||||
: mapper.ownedVisibleBalance(customerId, balanceId);
|
||||
if (balance == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory balance not found");
|
||||
}
|
||||
return balanceView(balance);
|
||||
}
|
||||
|
||||
public List<InventoryPositionView> positions(String customerId, long balanceId,
|
||||
InventoryQueryRequests.PositionQuery query) {
|
||||
validateStatus(query.inventoryStatus(), POSITION_STATUSES);
|
||||
|
||||
Reference in New Issue
Block a user