暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -0,0 +1,4 @@
package com.baiyue.lastleg.transfer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration @MapperScan("com.baiyue.lastleg.transfer.mapper") public class TransferMapperConfiguration {}
@@ -0,0 +1,93 @@
package com.baiyue.lastleg.transfer.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platform/v1/last-leg/warehouse-transfers")
public class WarehouseTransferController {
public static final String MANAGE_PERMISSION = "lastleg:warehouse-transfer:manage";
@Resource private WarehouseTransferService service;
@PostMapping
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @Valid @RequestBody WarehouseTransferRequests.Create request) {
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
}
@GetMapping("/{transferNo}")
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
return ApiResponse.success(service.detail(scope(user, customerId), transferNo));
}
@PostMapping("/{transferNo}/review")
public ApiResponse<WarehouseTransferResponses.Detail> review(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Review request) {
requireApprovalPermission(user);
return ApiResponse.success(service.review(scope(user, customerId), transferNo, operator(user), request));
}
@PostMapping("/{transferNo}/allocations")
public ApiResponse<WarehouseTransferResponses.Detail> allocate(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Allocate request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.allocate(scope(user, customerId), transferNo, operator(user), request));
}
@PostMapping("/{transferNo}/outbound")
public ApiResponse<WarehouseTransferResponses.Detail> outbound(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Outbound request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.outbound(scope(user, customerId), transferNo, operator(user), request));
}
@PostMapping("/{transferNo}/receipts")
public ApiResponse<WarehouseTransferResponses.Detail> receive(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.Receipt request) {
requirePermission(user, MANAGE_PERMISSION);
return ApiResponse.success(service.receive(scope(user, customerId), transferNo, operator(user), request));
}
private void requireApprovalPermission(AuthenticatedUser user) {
if (user == null || user.permissions() == null
|| !user.permissions().contains("lastleg:warehouse-transfer:approve")) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Warehouse transfer approval permission is required");
}
}
private void requirePermission(AuthenticatedUser user, String permission) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (user.permissions() == null || !user.permissions().contains(permission)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, permission + " permission is required");
}
}
private String scope(AuthenticatedUser user, String requested) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
if (requested == null || requested.isBlank() || "0".equals(requested)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
}
return requested.trim();
}
return String.valueOf(user.tenantId());
}
private String operator(AuthenticatedUser user) {
return user.userId() == null ? user.username() : String.valueOf(user.userId());
}
}
@@ -0,0 +1,76 @@
package com.baiyue.lastleg.transfer.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.DecimalMin;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
public final class WarehouseTransferRequests {
private WarehouseTransferRequests() {}
public record Create(
@NotBlank @Size(max = 128) String inboundCode,
@NotEmpty @Size(max = 200) List<@NotNull Long> sourceLastlegOrderIds,
@NotBlank @Size(max = 128) String operatingWarehouseId,
@NotBlank @Size(max = 128) String fbaWarehouseCode,
@NotBlank @Size(max = 1000) String deliveryAddress,
@Positive int packageCount,
@NotNull @DecimalMin(value = "0.0001") BigDecimal volumeCbm,
@NotBlank @Size(max = 64) String deliveryMethod,
@NotNull LocalDateTime estimatedArrivalAt,
@NotBlank @Size(max = 128) String fbaShipmentNo,
@Size(max = 255) String trackingNo,
@Size(max = 2000) String fbaBoxNos,
@Size(max = 1000) String remark,
boolean needRelabel,
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 500) List<@Valid Item> items
) {}
public record Review(@NotBlank @Size(max = 32) String decision,
@Size(max = 1000) String remark) {}
public record Allocate(
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 500) List<@Valid Allocation> allocations
) {}
public record Allocation(
@NotNull Long inventoryPositionId,
@NotNull Long palletId,
@Positive int packageCount
) {}
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
public record Receipt(
@NotNull Long sourceInventoryPositionId,
@NotBlank @Size(max = 128) String destinationOverseasWarehouseId,
@Size(max = 128) String destinationOverseasWarehouseCode,
@Size(max = 255) String destinationOverseasWarehouseName,
@Size(max = 128) String destinationWarehouseId,
@NotBlank @Size(max = 128) String destinationWarehouseCode,
@Size(max = 255) String destinationWarehouseName,
@NotNull Long destinationPalletId,
@NotBlank @Size(max = 128) String destinationPalletNo,
@Size(max = 128) String batchNo,
Long warehouseLocationId,
@Size(max = 128) String warehouseLocationCode,
@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark
) {}
public record Item(
@NotBlank @Size(max = 128) String skuCode,
@NotBlank @Size(max = 255) String productNameZh,
@Positive int packageCount,
@Size(max = 255) String shippingMark
) {}
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.transfer.dto;
import java.time.LocalDateTime;
import java.util.List;
public final class WarehouseTransferResponses {
private WarehouseTransferResponses() {}
public record Item(String skuCode, String productNameZh, int packageCount, String shippingMark) {}
public record SourceOrder(Long lastlegOrderId, String lastlegOrderNo, Long inboundOrderId, String inboundCode) {}
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
Long palletId, String palletNo, int packageCount, String status,
Long destinationInventoryPositionId) {}
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
String operatingWarehouseId, String fbaWarehouseCode, String deliveryAddress,
int packageCount, String deliveryMethod, LocalDateTime estimatedArrivalAt,
String fbaShipmentNo, String trackingNo, String fbaBoxNos, String remark,
List<SourceOrder> sourceOrders, List<Item> items, List<Allocation> allocations,
LocalDateTime outboundAt, LocalDateTime createdAt) {}
}
@@ -0,0 +1,30 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_inventory_allocation")
public class WarehouseTransferInventoryAllocation {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private String customerId;
private Long inventoryPositionId;
private Long inventoryBalanceId;
private Long inboundOrderId;
private String inboundCode;
private Long palletId;
private String palletNo;
private Integer packageCount;
private String status;
private String activePositionKey;
private LocalDateTime allocatedAt;
private LocalDateTime shippedAt;
private LocalDateTime receivedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,20 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_item")
public class WarehouseTransferItem {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private String customerId;
private String skuCode;
private String productNameZh;
private Integer packageCount;
private String shippingMark;
private LocalDateTime createdAt;
}
@@ -0,0 +1,43 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_order")
public class WarehouseTransferOrder {
@TableId(type = IdType.INPUT) private Long id;
private String customerId;
private String transferNo;
private Long inboundOrderId;
private String inboundCode;
private Long lastlegOrderId;
private String lastlegOrderNo;
private String operatingWarehouseId;
private String fbaWarehouseCode;
private String deliveryAddress;
private Integer packageCount;
private String deliveryMethod;
private LocalDateTime estimatedArrivalAt;
private String fbaShipmentNo;
private String trackingNo;
private String fbaBoxNos;
private String remark;
private Boolean needRelabel;
private Long relabelApplicationId;
private String relabelNo;
private String status;
private String allocationIdempotencyKey;
private String allocationRequestHash;
private String outboundIdempotencyKey;
private String outboundRequestHash;
private LocalDateTime outboundAt;
private String idempotencyKey;
private String requestHash;
private String createdBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,27 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_receipt")
public class WarehouseTransferReceipt {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private Long allocationId;
private String customerId;
private Long sourceInventoryPositionId;
private Long destinationInventoryBalanceId;
private Long destinationInventoryPositionId;
private Integer packageCount;
private String idempotencyKey;
private String requestHash;
private String inventoryOperationKey;
private String createdBy;
private LocalDateTime receivedAt;
private LocalDateTime createdAt;
}
@@ -0,0 +1,20 @@
package com.baiyue.lastleg.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_warehouse_transfer_source_order")
public class WarehouseTransferSourceOrder {
@TableId(type = IdType.INPUT) private Long id;
private Long transferOrderId;
private String customerId;
private Long sourceLastlegOrderId;
private String sourceLastlegOrderNo;
private Long inboundOrderId;
private String inboundCode;
private LocalDateTime createdAt;
}
@@ -0,0 +1,10 @@
package com.baiyue.lastleg.transfer.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper public interface InboundSnapshotMapper {
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND (inbound_code=#{code} OR source_inbound_id=#{code}) LIMIT 1")
Long findId(@Param("customerId") String customerId, @Param("code") String code);
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND source_inbound_id=#{sourceInboundId} LIMIT 1")
Long findBySourceInboundId(@Param("customerId") String customerId, @Param("sourceInboundId") String sourceInboundId);
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface WarehouseTransferInventoryAllocationMapper extends BaseMapper<WarehouseTransferInventoryAllocation> {
@Select("SELECT * FROM ll_warehouse_transfer_inventory_allocation WHERE customer_id=#{customerId} " +
"AND transfer_order_id=#{transferOrderId} ORDER BY id FOR UPDATE")
List<WarehouseTransferInventoryAllocation> lockTransfer(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId);
@Select("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE customer_id=#{customerId} " +
"AND warehouse_pallet_id=#{palletId} AND status='ACTIVE'")
int countActiveDispatchLock(@Param("customerId") String customerId, @Param("palletId") Long palletId);
}
@@ -0,0 +1,5 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface WarehouseTransferItemMapper extends BaseMapper<WarehouseTransferItem> {}
@@ -0,0 +1,14 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper public interface WarehouseTransferOrderMapper extends BaseMapper<WarehouseTransferOrder> {
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
WarehouseTransferOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1")
WarehouseTransferOrder findNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND transfer_no=#{transferNo} LIMIT 1 FOR UPDATE")
WarehouseTransferOrder lockNo(@Param("customerId") String customerId, @Param("transferNo") String transferNo);
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferReceipt;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface WarehouseTransferReceiptMapper extends BaseMapper<WarehouseTransferReceipt> {
@Select("SELECT * FROM ll_warehouse_transfer_receipt WHERE customer_id=#{customerId} " +
"AND transfer_order_id=#{transferOrderId} AND source_inventory_position_id=#{sourcePositionId} LIMIT 1")
WarehouseTransferReceipt findSource(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId,
@Param("sourcePositionId") Long sourcePositionId);
}
@@ -0,0 +1,5 @@
package com.baiyue.lastleg.transfer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface WarehouseTransferSourceOrderMapper extends BaseMapper<WarehouseTransferSourceOrder> {}
@@ -0,0 +1,536 @@
package com.baiyue.lastleg.transfer.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferItem;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferReceipt;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferSourceOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HexFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class WarehouseTransferService {
public static final String ORDER_TYPE = "WAREHOUSE_TRANSFER";
public static final String RETURN_RELABEL_ORDER_TYPE = "RETURN_RELABEL";
public static final String PENDING_REVIEW = "PENDING_REVIEW";
public static final String APPROVED = "APPROVED";
public static final String REJECTED = "REJECTED";
public static final String PENDING_ASSIGNMENT = "PENDING_ASSIGNMENT";
public static final String ALLOCATED = "ALLOCATED";
public static final String OUTBOUNDED = "OUTBOUNDED";
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
public static final String RECEIVED = "RECEIVED";
@Resource private WarehouseTransferOrderMapper transfers;
@Resource private WarehouseTransferItemMapper transferItems;
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
@Resource private WarehouseTransferInventoryAllocationMapper inventoryAllocations;
@Resource private WarehouseTransferReceiptMapper transferReceipts;
@Resource private InboundSnapshotMapper inboundSnapshots;
@Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderDetailMapper orderDetails;
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
@Resource private RelabelApplicationMapper relabelApplications;
@Resource private RelabelItemMapper relabelItems;
@Resource private RelabelStatusHistoryMapper relabelHistories;
@Resource private LastlegOrderNoGenerator orderNoGenerator;
@Resource private InventoryCommandService inventory;
@Resource private InventoryCommandMapper inventoryCommands;
@Resource private ObjectMapper objectMapper;
@Transactional
public WarehouseTransferResponses.Detail create(String customerId, String operatorId,
WarehouseTransferRequests.Create request) {
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
if (itemPackages != request.packageCount()) {
throw badRequest("Cargo item package total must equal transfer package count");
}
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
if (replay != null) {
if (!requestHash.equals(replay.getRequestHash())) {
throw conflict("Idempotency key was reused with different warehouse transfer data");
}
return detail(customerId, replay.getTransferNo());
}
String inboundCode = request.inboundCode().trim();
Long inboundOrderId = inboundSnapshots.findId(customerId, inboundCode);
if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode);
List<Long> selectedIds = request.sourceLastlegOrderIds().stream().distinct().toList();
List<LastlegOrder> selectedOrders = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds));
if (selectedOrders.size() != selectedIds.size()) {
throw notFound("One or more selected customer orders do not exist in the current tenant");
}
LocalDateTime now = LocalDateTime.now();
long transferId = IdWorker.getId();
String transferNo = "WTR" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + transferId;
LastlegOrder order = createTailOrder(customerId, inboundOrderId, transferNo, request, requestHash, now);
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
transfer.setId(transferId); transfer.setCustomerId(customerId); transfer.setTransferNo(transferNo);
transfer.setInboundOrderId(inboundOrderId); transfer.setInboundCode(inboundCode);
transfer.setLastlegOrderId(order.getId()); transfer.setLastlegOrderNo(order.getOrderNo());
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
transfer.setFbaWarehouseCode(request.fbaWarehouseCode().trim());
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
transfer.setFbaShipmentNo(request.fbaShipmentNo().trim()); transfer.setTrackingNo(trim(request.trackingNo()));
transfer.setFbaBoxNos(trim(request.fbaBoxNos())); transfer.setRemark(trim(request.remark()));
transfer.setNeedRelabel(request.needRelabel()); transfer.setStatus(PENDING_REVIEW);
transfer.setIdempotencyKey(key); transfer.setRequestHash(requestHash); transfer.setCreatedBy(trim(operatorId));
transfer.setCreatedAt(now); transfer.setUpdatedAt(now);
transfers.insert(transfer);
for (LastlegOrder selectedOrder : selectedOrders) {
Long selectedInboundId = StringUtils.hasText(selectedOrder.getSourceInboundId())
? inboundSnapshots.findBySourceInboundId(customerId, selectedOrder.getSourceInboundId()) : null;
if (selectedInboundId == null) selectedInboundId = inboundSnapshots.findId(customerId, selectedOrder.getInboundCode());
if (selectedInboundId == null) {
throw conflict("Selected order " + selectedOrder.getOrderNo() + " has no linked inbound order");
}
WarehouseTransferSourceOrder link = new WarehouseTransferSourceOrder();
link.setId(IdWorker.getId()); link.setTransferOrderId(transferId); link.setCustomerId(customerId);
link.setSourceLastlegOrderId(selectedOrder.getId()); link.setSourceLastlegOrderNo(selectedOrder.getOrderNo());
link.setInboundOrderId(selectedInboundId); link.setInboundCode(selectedOrder.getInboundCode()); link.setCreatedAt(now);
sourceOrders.insert(link);
}
for (int index = 0; index < request.items().size(); index++) {
WarehouseTransferRequests.Item source = request.items().get(index);
insertTransferItem(customerId, transferId, source, now);
insertOrderDetail(order.getId(), transferNo, index, source, request, now);
}
insertOrderHistory(order.getId(), null, PENDING_REVIEW, transferNo,
"Warehouse transfer submitted for review", now);
if (request.needRelabel()) createRelabel(customerId, operatorId, transfer, order, request, now);
return detail(customerId, transferNo);
}
@Transactional(readOnly = true)
public WarehouseTransferResponses.Detail detail(String customerId, String transferNo) {
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
List<WarehouseTransferResponses.Item> items = transferItems.selectList(
new LambdaQueryWrapper<WarehouseTransferItem>().eq(WarehouseTransferItem::getCustomerId, customerId)
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferItem::getId))
.stream().map(value -> new WarehouseTransferResponses.Item(value.getSkuCode(), value.getProductNameZh(),
value.getPackageCount(), value.getShippingMark())).toList();
List<WarehouseTransferResponses.SourceOrder> sources = sourceOrders.selectList(
new LambdaQueryWrapper<WarehouseTransferSourceOrder>().eq(WarehouseTransferSourceOrder::getCustomerId, customerId)
.eq(WarehouseTransferSourceOrder::getTransferOrderId, transfer.getId()).orderByAsc(WarehouseTransferSourceOrder::getId))
.stream().map(value -> new WarehouseTransferResponses.SourceOrder(value.getSourceLastlegOrderId(),
value.getSourceLastlegOrderNo(), value.getInboundOrderId(), value.getInboundCode())).toList();
Map<Long, WarehouseTransferReceipt> receiptByAllocation = transferReceipts.selectList(
new LambdaQueryWrapper<WarehouseTransferReceipt>().eq(WarehouseTransferReceipt::getCustomerId, customerId)
.eq(WarehouseTransferReceipt::getTransferOrderId, transfer.getId()))
.stream().collect(Collectors.toMap(WarehouseTransferReceipt::getAllocationId, Function.identity()));
List<WarehouseTransferResponses.Allocation> allocations = inventoryAllocations.selectList(
new LambdaQueryWrapper<WarehouseTransferInventoryAllocation>()
.eq(WarehouseTransferInventoryAllocation::getCustomerId, customerId)
.eq(WarehouseTransferInventoryAllocation::getTransferOrderId, transfer.getId())
.orderByAsc(WarehouseTransferInventoryAllocation::getId))
.stream().map(value -> {
WarehouseTransferReceipt receipt = receiptByAllocation.get(value.getId());
return new WarehouseTransferResponses.Allocation(value.getInventoryPositionId(), value.getInboundOrderId(),
value.getInboundCode(), value.getPalletId(), value.getPalletNo(), value.getPackageCount(),
value.getStatus(), receipt == null ? null : receipt.getDestinationInventoryPositionId());
}).toList();
String relabelStatus = null;
if (transfer.getRelabelNo() != null) {
RelabelApplication relabel = relabelApplications.findNo(customerId, transfer.getRelabelNo());
relabelStatus = relabel == null ? null : relabel.getStatus();
}
LastlegOrder tailOrder = orders.selectById(transfer.getLastlegOrderId());
return new WarehouseTransferResponses.Detail(transfer.getId(), transfer.getTransferNo(), transfer.getInboundOrderId(),
transfer.getInboundCode(), transfer.getLastlegOrderId(), transfer.getLastlegOrderNo(),
tailOrder == null ? ORDER_TYPE : tailOrder.getOrderType(),
tailOrder == null ? (transfer.getStatus() == null ? PENDING_REVIEW : transfer.getStatus()) : tailOrder.getCurrentStatus(),
Boolean.TRUE.equals(transfer.getNeedRelabel()), transfer.getRelabelApplicationId(), transfer.getRelabelNo(),
relabelStatus,
transfer.getOperatingWarehouseId(), transfer.getFbaWarehouseCode(), transfer.getDeliveryAddress(),
transfer.getPackageCount(), transfer.getDeliveryMethod(), transfer.getEstimatedArrivalAt(), transfer.getFbaShipmentNo(),
transfer.getTrackingNo(), transfer.getFbaBoxNos(), transfer.getRemark(), sources, items, allocations,
transfer.getOutboundAt(), transfer.getCreatedAt());
}
@Transactional
public WarehouseTransferResponses.Detail review(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.Review request) {
String decision = request.decision().trim().toUpperCase();
String next = switch (decision) {
case "APPROVE", APPROVED -> APPROVED;
case "REJECT", REJECTED -> REJECTED;
default -> throw badRequest("Review decision must be APPROVE or REJECT");
};
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (!PENDING_REVIEW.equals(transfer.getStatus())) {
if (next.equals(transfer.getStatus())) return detail(customerId, transferNo);
throw conflict("Warehouse transfer order was already reviewed");
}
LastlegOrder order = orders.selectById(transfer.getLastlegOrderId());
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing");
LocalDateTime now = LocalDateTime.now();
String before = order.getCurrentStatus();
transfer.setStatus(next); transfer.setUpdatedAt(now); transfers.updateById(transfer);
String orderStatus = APPROVED.equals(next) ? PENDING_ASSIGNMENT : "CANCELLED";
order.setCurrentStatus(orderStatus);
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + next);
order.setUpdatedAt(now); orders.updateById(order);
if (!java.util.Objects.equals(before, orderStatus)) {
insertOrderHistory(order.getId(), before, orderStatus, transferNo,
"Warehouse transfer reviewed: " + next, now);
}
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail allocate(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.Allocate request) {
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
if (ALLOCATED.equals(transfer.getStatus()) && key.equals(transfer.getAllocationIdempotencyKey())) {
if (!requestHash.equals(transfer.getAllocationRequestHash())) {
throw conflict("Allocation idempotency key was reused with different inventory positions");
}
return detail(customerId, transferNo);
}
if (!APPROVED.equals(transfer.getStatus())) {
throw conflict("Only an approved warehouse transfer can allocate inventory");
}
List<WarehouseTransferRequests.Allocation> requested = request.allocations().stream()
.sorted(java.util.Comparator.comparing(WarehouseTransferRequests.Allocation::inventoryPositionId)).toList();
Set<Long> uniquePositions = new HashSet<>();
long totalPackages = 0;
for (WarehouseTransferRequests.Allocation allocation : requested) {
if (!uniquePositions.add(allocation.inventoryPositionId())) {
throw badRequest("Inventory positions must be unique within a warehouse transfer");
}
totalPackages = Math.addExact(totalPackages, allocation.packageCount());
}
if (totalPackages != transfer.getPackageCount()) {
throw badRequest("Allocated package total must equal warehouse transfer package count");
}
Set<Long> sourceInboundIds = sourceOrders.selectList(new LambdaQueryWrapper<WarehouseTransferSourceOrder>()
.eq(WarehouseTransferSourceOrder::getCustomerId, customerId)
.eq(WarehouseTransferSourceOrder::getTransferOrderId, transfer.getId()))
.stream().map(WarehouseTransferSourceOrder::getInboundOrderId).collect(Collectors.toSet());
if (sourceInboundIds.isEmpty()) throw conflict("Warehouse transfer has no source inbound orders");
LocalDateTime now = LocalDateTime.now();
for (WarehouseTransferRequests.Allocation source : requested) {
InventoryPosition position = inventoryCommands.lockPosition(customerId, source.inventoryPositionId());
if (position == null) throw notFound("Inventory position not found: " + source.inventoryPositionId());
InventoryBalance balance = inventoryCommands.lockBalance(customerId, position.getBalanceId());
if (balance == null) throw conflict("Inventory position balance is missing");
if (!source.palletId().equals(position.getPalletId())) {
throw conflict("Inventory position does not belong to pallet " + source.palletId());
}
if (inventoryAllocations.countActiveDispatchLock(customerId, position.getPalletId()) > 0) {
throw conflict("Inventory pallet is already locked by an active dispatch");
}
if (!sourceInboundIds.contains(balance.getInboundOrderId())) {
throw conflict("Inventory position is not linked to a selected source inbound order");
}
if (packages(position.getAvailablePackages()) < source.packageCount()) {
throw conflict("Inventory position does not have enough available packages");
}
WarehouseTransferInventoryAllocation allocation = new WarehouseTransferInventoryAllocation();
allocation.setId(IdWorker.getId()); allocation.setTransferOrderId(transfer.getId());
allocation.setCustomerId(customerId); allocation.setInventoryPositionId(position.getId());
allocation.setInventoryBalanceId(balance.getId()); allocation.setInboundOrderId(balance.getInboundOrderId());
allocation.setInboundCode(balance.getInboundCode()); allocation.setPalletId(position.getPalletId());
allocation.setPalletNo(position.getPalletNo()); allocation.setPackageCount(source.packageCount());
allocation.setStatus("RESERVED"); allocation.setActivePositionKey(customerId + ":" + position.getId());
allocation.setAllocatedAt(now); allocation.setCreatedAt(now); allocation.setUpdatedAt(now);
try {
inventoryAllocations.insert(allocation);
} catch (DuplicateKeyException exception) {
throw conflict("Inventory position is already reserved by another warehouse transfer");
}
}
transfer.setStatus(ALLOCATED); transfer.setAllocationIdempotencyKey(key);
transfer.setAllocationRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer);
String before = order.getCurrentStatus(); order.setCurrentStatus("WAIT_OUTBOUND");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:ALLOCATED"); order.setUpdatedAt(now);
orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer inventory allocated", now);
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail outbound(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.Outbound request) {
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
if (Set.of(OUTBOUNDED, PARTIALLY_RECEIVED, RECEIVED).contains(transfer.getStatus())
&& key.equals(transfer.getOutboundIdempotencyKey())) {
if (!requestHash.equals(transfer.getOutboundRequestHash())) {
throw conflict("Outbound idempotency key was reused with different data");
}
return detail(customerId, transferNo);
}
if (!ALLOCATED.equals(transfer.getStatus())) {
throw conflict("Warehouse transfer inventory must be allocated before outbound");
}
List<WarehouseTransferInventoryAllocation> allocations = inventoryAllocations.lockTransfer(customerId, transfer.getId());
if (allocations.isEmpty() || allocations.stream().mapToLong(WarehouseTransferInventoryAllocation::getPackageCount).sum()
!= transfer.getPackageCount()) {
throw conflict("Warehouse transfer inventory allocation is incomplete");
}
LocalDateTime now = LocalDateTime.now();
for (WarehouseTransferInventoryAllocation allocation : allocations) {
if (!"RESERVED".equals(allocation.getStatus())) throw conflict("Warehouse transfer allocation was already processed");
String keyBase = "WAREHOUSE_TRANSFER:" + transferNo + ":" + allocation.getInventoryPositionId();
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(customerId,
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark()));
inventory.allocate(customerId, operatorId, move);
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark())));
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark())));
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation);
}
transfer.setStatus(OUTBOUNDED); transfer.setOutboundIdempotencyKey(key);
transfer.setOutboundRequestHash(requestHash); transfer.setOutboundAt(now); transfer.setUpdatedAt(now);
transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer);
String before = order.getCurrentStatus(); order.setCurrentStatus("IN_TRANSIT");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:OUTBOUNDED"); order.setUpdatedAt(now);
orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer left source warehouse", now);
return detail(customerId, transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail receive(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.Receipt request) {
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (!Set.of(OUTBOUNDED, PARTIALLY_RECEIVED).contains(transfer.getStatus())) {
if (RECEIVED.equals(transfer.getStatus())) {
WarehouseTransferReceipt replay = transferReceipts.findSource(customerId, transfer.getId(),
request.sourceInventoryPositionId());
if (replay != null && replay.getRequestHash().equals(hash(request))) return detail(customerId, transferNo);
}
throw conflict("Only outbound warehouse transfer inventory can be received");
}
List<WarehouseTransferInventoryAllocation> allocations = inventoryAllocations.lockTransfer(customerId, transfer.getId());
WarehouseTransferInventoryAllocation allocation = allocations.stream()
.filter(value -> request.sourceInventoryPositionId().equals(value.getInventoryPositionId()))
.findFirst().orElseThrow(() -> notFound("Source inventory position is not allocated to this transfer"));
String requestHash = hash(request);
WarehouseTransferReceipt existing = transferReceipts.findSource(customerId, transfer.getId(),
request.sourceInventoryPositionId());
if (existing != null) {
if (!requestHash.equals(existing.getRequestHash())) {
throw conflict("Transfer source position was already received with different destination data");
}
return detail(customerId, transferNo);
}
if (!"SHIPPED".equals(allocation.getStatus())) throw conflict("Transfer allocation is not awaiting receipt");
String operationKey = "WAREHOUSE_TRANSFER_RECEIPT:" + transferNo + ":" + allocation.getInventoryPositionId();
InventoryCommandResponse result = inventory.stockIn(customerId, operatorId,
new InventoryCommandRequests.StockIn(customerId, null,
request.destinationOverseasWarehouseId().trim(), trim(request.destinationOverseasWarehouseCode()),
trim(request.destinationOverseasWarehouseName()), trim(request.destinationWarehouseId()),
request.destinationWarehouseCode().trim(), trim(request.destinationWarehouseName()),
transfer.getLastlegOrderId(), transfer.getLastlegOrderNo(), transferNo,
request.destinationPalletId(), request.destinationPalletNo().trim(),
trim(request.batchNo()) == null ? transferNo : request.batchNo().trim(), request.warehouseLocationId(),
trim(request.warehouseLocationCode()), allocation.getPackageCount(), transferNo,
"WAREHOUSE_TRANSFER_RECEIPT:" + transferNo, operationKey, trim(request.remark())));
LocalDateTime now = LocalDateTime.now();
WarehouseTransferReceipt receipt = new WarehouseTransferReceipt();
receipt.setId(IdWorker.getId()); receipt.setTransferOrderId(transfer.getId()); receipt.setAllocationId(allocation.getId());
receipt.setCustomerId(customerId); receipt.setSourceInventoryPositionId(allocation.getInventoryPositionId());
receipt.setDestinationInventoryBalanceId(result.balanceId()); receipt.setDestinationInventoryPositionId(result.positionId());
receipt.setPackageCount(allocation.getPackageCount()); receipt.setIdempotencyKey(request.idempotencyKey().trim());
receipt.setRequestHash(requestHash); receipt.setInventoryOperationKey(operationKey); receipt.setCreatedBy(trim(operatorId));
receipt.setReceivedAt(now); receipt.setCreatedAt(now); transferReceipts.insert(receipt);
allocation.setStatus("RECEIVED"); allocation.setReceivedAt(now); allocation.setUpdatedAt(now);
inventoryAllocations.updateById(allocation);
boolean complete = allocations.stream().allMatch(value -> value.getId().equals(allocation.getId())
|| "RECEIVED".equals(value.getStatus()));
transfer.setStatus(complete ? RECEIVED : PARTIALLY_RECEIVED); transfer.setUpdatedAt(now); transfers.updateById(transfer);
LastlegOrder order = requireTailOrder(customerId, transfer);
String before = order.getCurrentStatus(); order.setCurrentStatus(complete ? "COMPLETED" : "IN_TRANSIT");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + (complete ? RECEIVED : PARTIALLY_RECEIVED));
order.setUpdatedAt(now); orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer inventory received at destination", now);
return detail(customerId, transferNo);
}
private LastlegOrder createTailOrder(String customerId, Long inboundOrderId, String transferNo,
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next()); order.setCustomerId(customerId);
order.setOrderType(request.needRelabel() ? RETURN_RELABEL_ORDER_TYPE : ORDER_TYPE);
order.setInboundCode(request.inboundCode().trim()); order.setSourceRegion("CUSTOMER");
order.setSourceInboundId("WAREHOUSE_TRANSFER:" + transferNo); order.setRootInboundId(String.valueOf(inboundOrderId));
order.setSourceOrderNo(transferNo); order.setOverseasWarehouseId(request.operatingWarehouseId().trim());
order.setFbaWarehouseCode(request.fbaWarehouseCode().trim()); order.setPackageCount(request.packageCount());
order.setVolumeCbm(request.volumeCbm());
order.setContainerNo(transferNo);
order.setPalletCount(0); order.setPickedPackageCount(0); order.setShippedPackageCount(0);
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW);
order.setCustomerRemark(trim(request.remark()));
order.setSourceEventId("WAREHOUSE_TRANSFER:" + transferNo); order.setSourceLoadingPlanId(ORDER_TYPE);
order.setSourcePayloadHash("sha256:" + requestHash); order.setCreatedAt(now); order.setUpdatedAt(now);
orders.insert(order);
return order;
}
private void insertTransferItem(String customerId, long transferId, WarehouseTransferRequests.Item source,
LocalDateTime now) {
WarehouseTransferItem item = new WarehouseTransferItem();
item.setId(IdWorker.getId()); item.setTransferOrderId(transferId); item.setCustomerId(customerId);
item.setSkuCode(source.skuCode().trim()); item.setProductNameZh(source.productNameZh().trim());
item.setPackageCount(source.packageCount()); item.setShippingMark(trim(source.shippingMark())); item.setCreatedAt(now);
transferItems.insert(item);
}
private void insertOrderDetail(long orderId, String transferNo, int index, WarehouseTransferRequests.Item source,
WarehouseTransferRequests.Create request, LocalDateTime now) {
LastlegOrderDetail detail = new LastlegOrderDetail();
detail.setOrderId(orderId); detail.setSourceInboundDetailId(transferNo + ":" + index);
detail.setSkuCode(source.skuCode().trim()); detail.setProductNameZh(source.productNameZh().trim());
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(request.fbaWarehouseCode().trim());
detail.setFbaTrackingNo(trim(request.trackingNo())); detail.setShipmentNo(request.fbaShipmentNo().trim());
detail.setShippingMark(trim(source.shippingMark())); orderDetails.insert(detail);
}
private void createRelabel(String customerId, String operatorId, WarehouseTransferOrder transfer, LastlegOrder order,
WarehouseTransferRequests.Create request, LocalDateTime now) {
long applicationId = IdWorker.getId();
String relabelNo = "FBR" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + applicationId;
RelabelApplication application = new RelabelApplication();
application.setId(applicationId); application.setCustomerId(customerId); application.setRelabelNo(relabelNo);
application.setLastlegOrderId(order.getId()); application.setCustomerOrderNo(transfer.getTransferNo());
application.setOverseasWarehouseId(request.operatingWarehouseId().trim()); application.setLabelDeadline(request.estimatedArrivalAt());
application.setContactName(StringUtils.hasText(operatorId) ? operatorId.trim() : "SYSTEM");
application.setContactPhone("N/A"); application.setRemark(trim(request.remark()));
application.setStatus(RelabelApplicationService.PENDING_PROCESSING); application.setItemCount(request.items().size());
application.setTotalPackageCount(request.packageCount()); application.setNotifiedPackageCount(request.packageCount());
application.setIdempotencyKey("WAREHOUSE_TRANSFER:" + request.idempotencyKey().trim());
application.setRequestHash(hash(request)); application.setCreatedBy(trim(operatorId));
application.setCreatedAt(now); application.setUpdatedAt(now); relabelApplications.insert(application);
for (WarehouseTransferRequests.Item source : request.items()) {
RelabelItem item = new RelabelItem();
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
item.setSkuCode(source.skuCode().trim()); item.setProductNameZh(source.productNameZh().trim());
item.setTotalPackageCount(source.packageCount()); item.setNotifiedPackageCount(source.packageCount());
item.setShipmentNo(request.fbaShipmentNo().trim()); item.setTrackingCode(trim(request.trackingNo()));
item.setNewSkuCode(source.skuCode().trim()); item.setLabelRequirement("WAREHOUSE_TRANSFER_RELABEL");
item.setCreatedAt(now); relabelItems.insert(item);
}
RelabelStatusHistory history = new RelabelStatusHistory();
history.setId(IdWorker.getId()); history.setCustomerId(customerId); history.setApplicationId(applicationId);
history.setToStatus(RelabelApplicationService.PENDING_PROCESSING); history.setOperatorId(trim(operatorId));
history.setRemark("Created from warehouse transfer " + transfer.getTransferNo()); history.setCreatedAt(now);
relabelHistories.insert(history);
transfer.setRelabelApplicationId(applicationId); transfer.setRelabelNo(relabelNo); transfer.setUpdatedAt(now);
transfers.updateById(transfer);
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(RelabelApplicationService.PENDING_PROCESSING);
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + PENDING_REVIEW + ";RELABEL:" + RelabelApplicationService.PENDING_PROCESSING);
order.setUpdatedAt(now); orders.updateById(order);
}
private void insertOrderHistory(long orderId, String fromStatus, String toStatus, String transferNo,
String remark, LocalDateTime now) {
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
history.setOrderId(orderId); history.setFromStatus(fromStatus); history.setToStatus(toStatus);
history.setEventId("WAREHOUSE_TRANSFER:" + transferNo); history.setRemark(remark);
history.setCreatedAt(now); orderHistories.insert(history);
}
private LastlegOrder requireTailOrder(String customerId, WarehouseTransferOrder transfer) {
LastlegOrder order = orders.selectById(transfer.getLastlegOrderId());
if (order == null || !customerId.equals(order.getCustomerId())) {
throw conflict("Linked tail-leg order is missing");
}
return order;
}
private String hash(Object value) {
try {
byte[] json = objectMapper.copy().findAndRegisterModules().writeValueAsString(value).getBytes(StandardCharsets.UTF_8);
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json));
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
private static long packages(Long value) { return value == null ? 0L : value; }
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
}