Merge origin/dev and resolve backend conflicts

This commit is contained in:
2026-09-08 20:50:28 +08:00
342 changed files with 13161 additions and 2296 deletions
@@ -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) { }
}
@@ -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);
}
}
@@ -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")
@@ -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);
@@ -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,
@@ -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,
@@ -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 &gt; 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>" +
@@ -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);