Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java
This commit is contained in:
+200
@@ -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;
|
||||
@@ -26,8 +28,11 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@@ -245,6 +250,119 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnedInventoryRestoreResponse restoreReturnedInventory(String customerId, String operatorId,
|
||||
ReturnedInventoryRestoreRequest request) {
|
||||
String exceptionNo = required(request.exceptionNo(), "Return exception number is required");
|
||||
String outboundNo = required(request.outboundNo(), "Return outbound order number is required");
|
||||
String warehouseId = required(request.overseasWarehouseId(), "Return warehouse is required");
|
||||
String idempotencyKey = required(request.idempotencyKey(), "Idempotency key is required");
|
||||
String requestHash = hash(request);
|
||||
InventoryOperation existing = operations.findIdempotency(customerId, idempotencyKey);
|
||||
if (existing != null) return replayReturned(existing, requestHash);
|
||||
if (request.sources() == null || request.sources().isEmpty()) {
|
||||
throw badRequest("Return inventory sources are required");
|
||||
}
|
||||
if (request.receipts() == null || request.receipts().isEmpty()) {
|
||||
throw badRequest("Return inventory receipts are required");
|
||||
}
|
||||
|
||||
Map<String, ReturnedInventoryRestoreRequest.SourceItem> sources = new LinkedHashMap<>();
|
||||
for (ReturnedInventoryRestoreRequest.SourceItem source : request.sources()) {
|
||||
if (source.outboundPalletItemId() == null || source.inventoryPositionId() == null
|
||||
|| source.maxReturnPackages() <= 0) {
|
||||
throw badRequest("Return inventory source is incomplete");
|
||||
}
|
||||
String key = returnSourceKey(source.outboundPalletItemId(), source.inventoryPositionId());
|
||||
if (sources.putIfAbsent(key, source) != null) throw badRequest("Return inventory source is duplicated");
|
||||
}
|
||||
Map<String, Long> receiptTotals = new LinkedHashMap<>();
|
||||
for (ReturnedInventoryRestoreRequest.Receipt receipt : request.receipts()) {
|
||||
if (receipt.outboundPalletItemId() == null || receipt.inventoryPositionId() == null
|
||||
|| receipt.receivedPackages() <= 0 || receipt.targetPalletId() == null
|
||||
|| receipt.targetLocationId() == null || trim(receipt.targetPalletNo()) == null
|
||||
|| trim(receipt.targetLocationCode()) == null) {
|
||||
throw badRequest("Return inventory receipt is incomplete");
|
||||
}
|
||||
String key = returnSourceKey(receipt.outboundPalletItemId(), receipt.inventoryPositionId());
|
||||
if (!sources.containsKey(key)) throw conflict("Return inventory receipt does not match an approved source");
|
||||
receiptTotals.merge(key, receipt.receivedPackages(), Math::addExact);
|
||||
}
|
||||
|
||||
InventoryOperation operation = new InventoryOperation();
|
||||
operation.setId(IdWorker.getId());
|
||||
operation.setCustomerId(customerId);
|
||||
operation.setIdempotencyKey(idempotencyKey);
|
||||
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, idempotencyKey);
|
||||
if (concurrent != null) return replayReturned(concurrent, requestHash);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
Map<String, Locked> lockedSources = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ReturnedInventoryRestoreRequest.SourceItem> entry : sources.entrySet()) {
|
||||
ReturnedInventoryRestoreRequest.SourceItem source = entry.getValue();
|
||||
Locked locked = locked(customerId, source.inventoryPositionId());
|
||||
if (!Objects.equals(locked.balance().getInboundOrderId(), request.inboundOrderId())
|
||||
|| !warehouseId.equals(trim(locked.balance().getOverseasWarehouseId()))) {
|
||||
throw conflict("Return inventory source does not belong to the exception inbound order and warehouse");
|
||||
}
|
||||
String sourceIdentity = returnSourceIdentity(exceptionNo, source);
|
||||
long shipped = commands.shippedPackages(customerId, source.inventoryPositionId(), outboundNo,
|
||||
sku(source.skuCode()));
|
||||
long restored = commands.restoredPackages(customerId, sourceIdentity, sku(source.skuCode()));
|
||||
long allowed = Math.min(source.maxReturnPackages(), shipped) - restored;
|
||||
long requested = receiptTotals.getOrDefault(entry.getKey(), 0L);
|
||||
if (requested > allowed) throw conflict("Return inventory receipt exceeds the remaining shipped packages");
|
||||
lockedSources.put(entry.getKey(), locked);
|
||||
}
|
||||
|
||||
List<Long> ledgerIds = new ArrayList<>();
|
||||
long beforePackages = 0;
|
||||
long restoredPackages = 0;
|
||||
int sequence = 0;
|
||||
String batchNo = ("RETURN-" + exceptionNo).substring(0, Math.min(128, 7 + exceptionNo.length()));
|
||||
for (ReturnedInventoryRestoreRequest.Receipt receipt : request.receipts()) {
|
||||
String key = returnSourceKey(receipt.outboundPalletItemId(), receipt.inventoryPositionId());
|
||||
ReturnedInventoryRestoreRequest.SourceItem source = sources.get(key);
|
||||
Locked locked = lockedSources.get(key);
|
||||
InventoryBalance balance = locked.balance();
|
||||
InventoryPosition target = returnPosition(customerId, balance, receipt, batchNo);
|
||||
long before = value(target.getAvailablePackages());
|
||||
beforePackages = Math.addExact(beforePackages, before);
|
||||
target.setAvailablePackages(Math.addExact(before, receipt.receivedPackages()));
|
||||
target.setStatus("OCCUPIED");
|
||||
target.setVersion(value(target.getVersion()) + 1);
|
||||
target.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(target);
|
||||
|
||||
balance.setAvailablePackages(Math.addExact(value(balance.getAvailablePackages()), receipt.receivedPackages()));
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
restoredPackages = Math.addExact(restoredPackages, receipt.receivedPackages());
|
||||
InventoryLedger ledger = returnLedger(customerId, operatorId, operation.getId(), balance, target,
|
||||
source, exceptionNo, outboundNo, returnSourceIdentity(exceptionNo, source), idempotencyKey,
|
||||
sequence++, before, receipt.receivedPackages(), request.evidenceFileIds());
|
||||
ledgers.insert(ledger);
|
||||
ledgerIds.add(ledger.getId());
|
||||
}
|
||||
LocalDateTime completedAt = LocalDateTime.now();
|
||||
ReturnedInventoryRestoreResponse response = new ReturnedInventoryRestoreResponse(operation.getId(), ledgerIds,
|
||||
beforePackages, restoredPackages, Math.addExact(beforePackages, restoredPackages), false, completedAt);
|
||||
operation.setStatus("COMPLETED");
|
||||
operation.setResultJson(write(response));
|
||||
operation.setUpdatedAt(completedAt);
|
||||
operations.updateById(operation);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjust(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request) {
|
||||
@@ -423,6 +541,42 @@ public class InventoryCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryPosition returnPosition(String customerId, InventoryBalance balance,
|
||||
ReturnedInventoryRestoreRequest.Receipt receipt, String batchNo) {
|
||||
InventoryPosition position = positions.lockPosition(customerId, balance.getId(), receipt.targetPalletId(), batchNo);
|
||||
if (position != null) {
|
||||
if (!required(receipt.targetPalletNo(), "Return pallet number is required").equals(position.getPalletNo())
|
||||
|| !Objects.equals(receipt.targetLocationId(), position.getWarehouseLocationId())
|
||||
|| !required(receipt.targetLocationCode(), "Return location code is required")
|
||||
.equals(position.getWarehouseLocationCode())) {
|
||||
throw conflict("Return target pallet or location changed for the same exception");
|
||||
}
|
||||
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(required(receipt.targetPalletNo(), "Return pallet number is required"));
|
||||
position.setBatchNo(batchNo);
|
||||
position.setWarehouseLocationId(receipt.targetLocationId());
|
||||
position.setWarehouseLocationCode(required(receipt.targetLocationCode(), "Return location code is required"));
|
||||
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());
|
||||
try {
|
||||
positions.insert(position);
|
||||
return position;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryPosition concurrent = positions.lockPosition(customerId, balance.getId(), receipt.targetPalletId(), batchNo);
|
||||
if (concurrent != null) return concurrent;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private Locked locked(String customerId, Long positionId) {
|
||||
InventoryPosition position = commands.lockPosition(customerId, positionId);
|
||||
if (position == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory position not found");
|
||||
@@ -483,6 +637,52 @@ public class InventoryCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
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 return inventory idempotency result", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryLedger returnLedger(String customerId, String operatorId, Long operationId,
|
||||
InventoryBalance balance, InventoryPosition position,
|
||||
ReturnedInventoryRestoreRequest.SourceItem source,
|
||||
String exceptionNo, String outboundNo, String sourceIdentity,
|
||||
String idempotencyKey, int sequence, long before, long delta,
|
||||
List<String> evidenceFileIds) {
|
||||
InventoryLedger ledger = new InventoryLedger();
|
||||
ledger.setId(IdWorker.getId()); ledger.setCustomerId(customerId); ledger.setCustomerName(balance.getCustomerName());
|
||||
ledger.setBalanceId(balance.getId()); ledger.setPositionId(position.getId());
|
||||
ledger.setOverseasWarehouseId(balance.getOverseasWarehouseId());
|
||||
ledger.setDestinationWarehouseCode(balance.getDestinationWarehouseCode()); ledger.setInboundOrderId(balance.getInboundOrderId());
|
||||
ledger.setPalletId(position.getPalletId()); ledger.setPalletNo(position.getPalletNo()); ledger.setBatchNo(position.getBatchNo());
|
||||
ledger.setWarehouseLocationCode(position.getWarehouseLocationCode()); ledger.setSkuCode(sku(source.skuCode()));
|
||||
ledger.setBusinessType("RETURN_STOCK_IN"); ledger.setBusinessOrderId(balance.getInboundOrderId());
|
||||
ledger.setBusinessOrderNo(exceptionNo); ledger.setInboundCode(balance.getInboundCode());
|
||||
ledger.setInboundOrderNo(balance.getInboundOrderNo()); ledger.setOutboundOrderNo(outboundNo);
|
||||
ledger.setSourceEventId(sourceIdentity); ledger.setIdempotencyKey(idempotencyKey + ":RETURN:" + sequence + ":" + operationId);
|
||||
ledger.setStatusFrom("RETURNED"); ledger.setStatusTo("AVAILABLE"); ledger.setBeforePackages(before);
|
||||
ledger.setDeltaPackages(delta); ledger.setAfterPackages(Math.addExact(before, delta)); ledger.setOperatorId(trim(operatorId));
|
||||
ledger.setRemark("Exception return received; evidenceCount=" + (evidenceFileIds == null ? 0 : evidenceFileIds.size()));
|
||||
ledger.setOccurredAt(LocalDateTime.now()); ledger.setCreatedAt(ledger.getOccurredAt());
|
||||
return ledger;
|
||||
}
|
||||
|
||||
private static String returnSourceKey(Long outboundPalletItemId, Long inventoryPositionId) {
|
||||
return outboundPalletItemId + ":" + inventoryPositionId;
|
||||
}
|
||||
|
||||
private static String returnSourceIdentity(String exceptionNo, ReturnedInventoryRestoreRequest.SourceItem source) {
|
||||
return "RETURN:" + exceptionNo + ":" + source.outboundPalletItemId() + ":" + source.inventoryPositionId();
|
||||
}
|
||||
|
||||
private void 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,
|
||||
|
||||
Reference in New Issue
Block a user