暂存提交
This commit is contained in:
+4
@@ -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 {}
|
||||
+93
@@ -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());
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
) {}
|
||||
}
|
||||
+21
@@ -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) {}
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
+43
@@ -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;
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
+5
@@ -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> {}
|
||||
+14
@@ -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);
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
+5
@@ -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> {}
|
||||
+536
@@ -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); }
|
||||
}
|
||||
Reference in New Issue
Block a user