fix(lastleg): keep inventory warehouse snapshots canonical
This commit is contained in:
+4
-1
@@ -228,6 +228,8 @@ public class InboundStockReceiptService {
|
||||
"Actual overseas warehouse is required for stock-in");
|
||||
String overseasWarehouseName = required(order.getOverseasWarehouseName(),
|
||||
"Actual overseas warehouse name is required for stock-in");
|
||||
String overseasWarehouseCode = required(order.getOverseasWarehouseCode(),
|
||||
"Actual overseas warehouse code is required for stock-in");
|
||||
if (plan.getOverseasWarehouseId() == null
|
||||
|| !overseasWarehouseId.equals(String.valueOf(plan.getOverseasWarehouseId()))) {
|
||||
throw conflict("Inbound plan and tail-leg order overseas warehouses must match for stock-in");
|
||||
@@ -236,7 +238,8 @@ public class InboundStockReceiptService {
|
||||
throw conflict("Actual overseas warehouse must not use the domestic source warehouse");
|
||||
}
|
||||
InventoryCommandRequests.StockIn stockIn = new InventoryCommandRequests.StockIn(null, customerName,
|
||||
overseasWarehouseId, null, overseasWarehouseName, line.getDestinationWarehouseId(), destinationCode,
|
||||
overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName,
|
||||
line.getDestinationWarehouseId(), destinationCode,
|
||||
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
|
||||
sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
|
||||
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
|
||||
|
||||
+2
-2
@@ -259,8 +259,8 @@ public class InboundPlanService {
|
||||
if (updated != 1) {
|
||||
throw new IllegalStateException("Failed to update order " + order.orderId());
|
||||
}
|
||||
inventoryBalanceMapper.bindInboundPlan(customerId, order.orderId(), order.inboundCode(),
|
||||
plan.getPlanNo(), now);
|
||||
inventoryBalanceMapper.bindInboundPlan(customerId, receivingWarehouseId, order.orderId(),
|
||||
order.inboundCode(), plan.getPlanNo(), now);
|
||||
orderMapper.insertHistory(customerId, order.orderId(), order.currentStatus(),
|
||||
"Inbound order converted to inbound plan " + plan.getPlanNo(), now);
|
||||
}
|
||||
|
||||
+15
@@ -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,20 @@ 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 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")
|
||||
|
||||
+71
-5
@@ -28,6 +28,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class InventoryCommandService {
|
||||
@@ -57,6 +58,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 +165,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 +175,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;
|
||||
}
|
||||
@@ -287,6 +332,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 +342,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 +354,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 +374,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;
|
||||
}
|
||||
@@ -505,6 +555,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 +640,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,
|
||||
|
||||
+3
-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,
|
||||
@@ -79,7 +79,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,
|
||||
|
||||
+4
-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>" +
|
||||
|
||||
@@ -140,6 +140,10 @@ public class LastlegOrderService {
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference warehouse =
|
||||
warehouseResourceGateway.requireActiveSharedWarehouse(request.overseasWarehouseId());
|
||||
for (LastlegOrder order : orders) {
|
||||
inventory.rebindReceivingWarehouse(order.getCustomerId(), order.getId(), warehouse.warehouseId(),
|
||||
warehouse.warehouseCode(), warehouse.warehouseName(), operatorName, key);
|
||||
}
|
||||
int updated = orderMapper.updateContainerReceivingWarehouse(bookingId,
|
||||
Long.toString(warehouse.warehouseId()), warehouse.warehouseCode(), warehouse.warehouseName(), version);
|
||||
if (updated != orders.size()) {
|
||||
|
||||
+3
-2
@@ -341,8 +341,9 @@ public class LoadingCompletedConsumer {
|
||||
FirstLegLoadingPlanCompletedV1.Item item = source.items().get(index);
|
||||
if (item.packageCount() <= 0) continue;
|
||||
inventory.registerInTransit(order.getCustomerId(), "SYSTEM",
|
||||
new InventoryCommandRequests.InTransit(order.getCustomerName(), payload.warehouseId(), null,
|
||||
payload.warehouseName(), item.destinationWarehouseId(), item.destinationWarehouseCode(),
|
||||
new InventoryCommandRequests.InTransit(order.getCustomerName(), order.getOverseasWarehouseId(),
|
||||
order.getOverseasWarehouseCode(), order.getOverseasWarehouseName(),
|
||||
item.destinationWarehouseId(), item.destinationWarehouseCode(),
|
||||
item.destinationWarehouseName(), order.getId(), inboundOrderNo, source.inboundCode(),
|
||||
item.skuCode(), item.productNameZh(), item.productNameEn(), item.packageCount(),
|
||||
order.getOrderNo(), event.eventId(),
|
||||
|
||||
Reference in New Issue
Block a user