贴标申请,仓储转运
This commit is contained in:
+5
-2
@@ -15,9 +15,12 @@ public class FleetCustomerDirectory {
|
||||
this.client = client; this.token = token;
|
||||
}
|
||||
public List<Customer> options(String keyword) { return load(keyword, null); }
|
||||
public Customer required(long id) {
|
||||
var customer = load(null, id).stream().filter(row -> row.id().equals(Long.toString(id))).findFirst()
|
||||
public Customer find(String id) {
|
||||
return load(null, Long.parseLong(id)).stream().filter(row -> row.id().equals(id)).findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "请选择有效客户"));
|
||||
}
|
||||
public Customer required(long id) {
|
||||
var customer = find(Long.toString(id));
|
||||
try {
|
||||
var result = client.eligibility(token, id);
|
||||
if (result == null || !result.successful() || result.data() == null) throw unavailable();
|
||||
|
||||
@@ -45,6 +45,18 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
|
||||
record OrderWeightRow(Long orderId, BigDecimal grossWeightKg, BigDecimal netWeightKg) { }
|
||||
|
||||
record ReturnVolumeRow(Long orderId, BigDecimal volumeCbm) { }
|
||||
|
||||
@Select("<script>SELECT m.orderId,SUM(m.length*m.width*m.height/1000000) AS volumeCbm FROM (" +
|
||||
"SELECT o.lastleg_order_id AS orderId,r.length,r.width,r.height," +
|
||||
"ROW_NUMBER() OVER (PARTITION BY r.return_order_id,r.unit_type,COALESCE(NULLIF(r.unit_code,''),r.receipt_no) " +
|
||||
"ORDER BY r.signed_at DESC,r.id DESC) AS measurementNo " +
|
||||
"FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id AND o.customer_id=r.customer_id " +
|
||||
"WHERE o.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND r.unit_type IN ('PALLET','BOX') AND r.length IS NOT NULL AND r.width IS NOT NULL AND r.height IS NOT NULL" +
|
||||
") m WHERE m.measurementNo=1 GROUP BY m.orderId</script>")
|
||||
List<ReturnVolumeRow> findReturnVolumes(@Param("orderIds") List<Long> orderIds);
|
||||
|
||||
@Select("<script>SELECT o.id AS orderId," +
|
||||
"COALESCE(SUM(d.gross_weight_kg),MAX(i.gross_weight_kg),0) AS grossWeightKg," +
|
||||
"COALESCE(SUM(d.net_weight_kg),MAX(i.net_weight_kg),0) AS netWeightKg " +
|
||||
|
||||
+28
-3
@@ -265,7 +265,6 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
"INBOUND", null, null, List.of(), 0);
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList());
|
||||
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
|
||||
if ("PARCEL".equals(carton.getSourceUnitType())) continue;
|
||||
String keyword = trim(inboundCode);
|
||||
if (StringUtils.hasText(keyword)
|
||||
&& !containsIgnoreCase(carton.getCartonMark(), keyword)
|
||||
@@ -274,7 +273,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.filter(item -> value(item.getQuantity()) > 0)
|
||||
.map(item -> {
|
||||
long used = returnCartons.relabelledQuantity(carton.getId(), item.getSkuCode());
|
||||
long remaining = value(item.getQuantity()) - used;
|
||||
long remaining = value(item.getQuantity()) - used - value(item.getPackingReservedQuantity());
|
||||
return new RelabelResponses.SkuItem(item.getSkuCode(), null,
|
||||
Math.toIntExact(Math.max(remaining, 0)), null);
|
||||
}).filter(item -> item.quantity() > 0).toList();
|
||||
@@ -288,7 +287,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
|
||||
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
|
||||
carton.getDestinationWarehouseName(), skuCodes, "退货入库库存", availablePackages, 0, List.of(),
|
||||
"RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages));
|
||||
"PARCEL".equals(carton.getSourceUnitType()) ? "RETURN_PARCEL" : "RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -392,6 +391,21 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
|
||||
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
|
||||
}
|
||||
if (!carton.getCartonMark().equals(item.cartonMark().trim()))
|
||||
throw conflict("来源退货箱号与选择的库存不一致");
|
||||
if ("PARCEL".equals(carton.getSourceUnitType()) && !"SKU_LABEL".equals(item.operationType()))
|
||||
throw conflict("散件退货库存只能贴 SKU,请先合箱后申请箱唛或混贴");
|
||||
final long sourceReturnCartonId = carton.getId();
|
||||
Map<String, Long> availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream()
|
||||
.collect(Collectors.toMap(line -> line.getSkuCode().toUpperCase(java.util.Locale.ROOT),
|
||||
line -> Math.max(0, value(line.getQuantity()) - value(line.getPackingReservedQuantity())
|
||||
- returnCartons.relabelledQuantity(sourceReturnCartonId, line.getSkuCode())), Long::sum));
|
||||
Map<String, Long> requestedSku = new java.util.HashMap<>();
|
||||
if (item.skuItems() != null) for (var skuItem : item.skuItems()) {
|
||||
long quantity = requestedSku.merge(skuItem.skuCode().toUpperCase(java.util.Locale.ROOT), (long) skuItem.quantity(), Long::sum);
|
||||
if (quantity > availableSku.getOrDefault(skuItem.skuCode().toUpperCase(java.util.Locale.ROOT), 0L))
|
||||
throw conflict("SKU " + skuItem.skuCode() + " 的换标数量超过来源箱可用数量");
|
||||
}
|
||||
if (returnCartonItems.lockCarton(carton.getId()).stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货箱SKU已被合箱计划锁定");
|
||||
if (Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(item.operationType())
|
||||
@@ -477,6 +491,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
throw conflict("退货箱状态已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
for (String returnNo : selectedReturnCartons.values().stream().map(ReturnInventoryCarton::getSourceReturnNo)
|
||||
.filter(StringUtils::hasText).distinct().sorted().toList()) {
|
||||
ReturnOrder returnOrder = returnOrders.lockCustomerNo(customerId, returnNo);
|
||||
if (returnOrder != null) {
|
||||
returnOrder.setRelabelApplicationId(applicationId);
|
||||
returnOrder.setRelabelNo(relabelNo);
|
||||
returnOrder.setRelabelStatus("PENDING_REVIEW");
|
||||
returnOrder.setUpdatedAt(now);
|
||||
returnOrders.updateById(returnOrder);
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresWallet) {
|
||||
String walletEventId = walletCommands.publish("FREEZE", Long.parseLong(customerId), "RELABEL_ORDER",
|
||||
|
||||
+5
@@ -24,6 +24,11 @@ public class MerchantReturnOrderController {
|
||||
@PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnOrderRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||
@PutMapping("/{returnNo}") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Create request) {
|
||||
return ApiResponse.success(service.updatePending(customer(user), returnNo, request));
|
||||
}
|
||||
@GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')")
|
||||
public ApiResponse<PageResult<ReturnOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ReturnOrderRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||
|
||||
+4
-2
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.lastleg.returnreceipt.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReturnOrderResponses {
|
||||
@@ -22,9 +23,10 @@ public final class ReturnOrderResponses {
|
||||
String skuCode, long quantity, LocalDateTime createdAt) { }
|
||||
public record Photo(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
public record PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items,
|
||||
long totalBoxCount, String unitType, String unitCode) {
|
||||
long totalBoxCount, String unitType, String unitCode,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight, BigDecimal volumeCbm) {
|
||||
public PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items) {
|
||||
this(palletCode, skuTypes, totalQuantity, items, 0, "PALLET", palletCode);
|
||||
this(palletCode, skuTypes, totalQuantity, items, 0, "PALLET", palletCode, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
public record PalletItem(String skuCode, String productNameZh, long quantity, long boxCount) {
|
||||
|
||||
+49
-4
@@ -78,6 +78,7 @@ public class ReturnReceiptService {
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway photoFiles;
|
||||
@Resource private com.baiyue.lastleg.service.FbaWarehouseValidator fbaWarehouseValidator;
|
||||
@Resource private com.baiyue.lastleg.service.ReturnOrderReferenceResolver returnOrderReferences;
|
||||
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail create(String customerId, String operatorId, ReturnOrderRequests.Create request) {
|
||||
@@ -130,9 +131,14 @@ public class ReturnReceiptService {
|
||||
tail.setSourceOrderNo(returnOrder.getReturnNo()); tail.setOverseasWarehouseId(returnOrder.getOverseasWarehouseId());
|
||||
tail.setCurrentStatus(PENDING_REVIEW); tail.setSubOrderStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? "RETURN_RELABEL:PENDING_REVIEW" : "RETURN_RELABEL:NOT_REQUIRED");
|
||||
tail.setRelabelStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? PENDING_REVIEW : "NOT_REQUIRED");
|
||||
// A return forecast is already the source of truth for the expected cargo.
|
||||
// Keep the tail-leg snapshot useful before PDA receipt facts exist.
|
||||
tail.setPackageCount(returnOrder.getExpectedQuantity() == null ? 0 : Math.toIntExact(returnOrder.getExpectedQuantity()));
|
||||
tail.setProductQuantity(returnOrder.getExpectedQuantity() == null ? 0 : Math.toIntExact(returnOrder.getExpectedQuantity()));
|
||||
tail.setCustomerRemark(returnOrder.getRemark()); tail.setSourceEventId("RETURN_ORDER:" + returnOrder.getReturnNo());
|
||||
tail.setSourceLoadingPlanId("RETURN_RELABEL"); tail.setCreatedAt(now); tail.setUpdatedAt(now);
|
||||
tail.setSourcePayloadHash(returnOrder.getRequestHash());
|
||||
returnOrderReferences.populate(List.of(tail));
|
||||
lastlegOrders.insert(tail);
|
||||
returnOrder.setLastlegOrderId(tail.getId()); returnOrder.setLastlegOrderNo(tail.getOrderNo());
|
||||
returnOrder.setRelabelStatus(Boolean.TRUE.equals(returnOrder.getNeedRelabel()) ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
@@ -207,12 +213,35 @@ public class ReturnReceiptService {
|
||||
if (order == null) return;
|
||||
Map<String, ReturnOrderItem> bySku = orderItems.listOrder(order.getId()).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(ReturnOrderItem::getSkuCode, value -> value, (left, right) -> left, LinkedHashMap::new));
|
||||
for (ReturnReceipt receipt : receipts.listOrder(order.getId())) {
|
||||
List<ReturnReceipt> returnReceipts = receipts.listOrder(order.getId());
|
||||
for (ReturnReceipt receipt : returnReceipts) {
|
||||
List<Allocation> allocations = receiptItems.listReceipt(receipt.getId()).stream()
|
||||
.map(line -> new Allocation(bySku.get(line.getSkuCode()), value(line.getReceivedQuantity())))
|
||||
.filter(item -> item.item() != null).toList();
|
||||
if (!allocations.isEmpty()) syncTailCargo(order, receipt, allocations, receipt.getSignedAt() == null ? LocalDateTime.now() : receipt.getSignedAt());
|
||||
}
|
||||
// Pending forecasts have no receipt rows yet, but their SKU forecast must
|
||||
// still be visible in the platform tail-order cargo tab.
|
||||
if (returnReceipts.isEmpty() && lastlegOrderDetails.selectCount(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, tailOrderId)) == 0) {
|
||||
int total = 0;
|
||||
for (ReturnOrderItem item : orderItems.listOrder(order.getId())) {
|
||||
int quantity = Math.toIntExact(value(item.getExpectedQuantity()));
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail();
|
||||
detail.setId(IdWorker.getId()); detail.setCustomerId(customerId); detail.setOrderId(tailOrderId);
|
||||
detail.setSourceInboundDetailId("RETURN_ORDER:" + order.getReturnNo() + ":" + item.getSkuCode());
|
||||
detail.setSkuCode(item.getSkuCode()); detail.setProductNameZh(item.getProductNameZh());
|
||||
detail.setPackageCount(quantity); detail.setProductQuantity(quantity);
|
||||
lastlegOrderDetails.insert(detail); total = Math.addExact(total, quantity);
|
||||
}
|
||||
if (total > 0) {
|
||||
LastlegOrder tail = lastlegOrders.selectById(tailOrderId);
|
||||
if (tail != null) {
|
||||
tail.setPackageCount(total); tail.setProductQuantity(total); tail.setUpdatedAt(LocalDateTime.now());
|
||||
lastlegOrders.updateById(tail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -243,12 +272,16 @@ public class ReturnReceiptService {
|
||||
Map<String, Map<String, ReturnOrderResponses.PalletItem>> palletItems = new LinkedHashMap<>();
|
||||
Map<String, String> palletTypes = new LinkedHashMap<>();
|
||||
Map<String, Long> physicalBoxes = new LinkedHashMap<>();
|
||||
Map<String, ReturnReceipt> physicalMeasurements = new LinkedHashMap<>();
|
||||
for (ReturnReceipt receipt : orderReceipts) {
|
||||
if (!"PALLET".equals(receipt.getUnitType()) && !"BOX".equals(receipt.getUnitType())) continue;
|
||||
String physicalCode = text(receipt.getUnitCode()) == null ? text(receipt.getPalletCode()) : text(receipt.getUnitCode());
|
||||
if (physicalCode == null) continue;
|
||||
String key = receipt.getUnitType() + ":" + physicalCode;
|
||||
palletTypes.put(key, receipt.getUnitType());
|
||||
// A pallet is one physical carrier even when signed in several batches.
|
||||
// Keep its latest measurement instead of adding the same carrier's volume twice.
|
||||
if (receipt.getLength() != null && receipt.getWidth() != null && receipt.getHeight() != null) physicalMeasurements.putIfAbsent(key, receipt);
|
||||
physicalBoxes.merge(key, value(receipt.getTotalBoxCount()), Long::sum);
|
||||
Map<String, ReturnOrderResponses.PalletItem> bySku = palletItems.computeIfAbsent(key, ignored -> new LinkedHashMap<>());
|
||||
for (ReturnReceiptItem line : receiptItems.listReceipt(receipt.getId())) bySku.merge(line.getSkuCode(),
|
||||
@@ -258,8 +291,12 @@ public class ReturnReceiptService {
|
||||
List<ReturnOrderResponses.PalletDetail> palletViews = palletItems.entrySet().stream().map(entry -> {
|
||||
List<ReturnOrderResponses.PalletItem> palletLines = List.copyOf(entry.getValue().values());
|
||||
String[] parts = entry.getKey().split(":", 2);
|
||||
ReturnReceipt measured = physicalMeasurements.get(entry.getKey());
|
||||
return new ReturnOrderResponses.PalletDetail(parts.length > 1 ? parts[1] : entry.getKey(), palletLines.size(), palletLines.stream().mapToLong(ReturnOrderResponses.PalletItem::quantity).sum(), palletLines,
|
||||
physicalBoxes.getOrDefault(entry.getKey(), 0L), palletTypes.get(entry.getKey()), parts.length > 1 ? parts[1] : entry.getKey());
|
||||
physicalBoxes.getOrDefault(entry.getKey(), 0L), palletTypes.get(entry.getKey()), parts.length > 1 ? parts[1] : entry.getKey(),
|
||||
measured == null ? null : measured.getLength(), measured == null ? null : measured.getWidth(),
|
||||
measured == null ? null : measured.getHeight(), measured == null ? null : measured.getWeight(),
|
||||
measured == null ? null : measured.getLength().multiply(measured.getWidth()).multiply(measured.getHeight()).divide(java.math.BigDecimal.valueOf(1_000_000)));
|
||||
}).toList();
|
||||
List<ReturnOrderResponses.Photo> photos = photoList(order).stream()
|
||||
.map(file -> new ReturnOrderResponses.Photo(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes())).toList();
|
||||
@@ -662,9 +699,17 @@ public class ReturnReceiptService {
|
||||
tail.setReceivedPackageCount(Math.addExact(tail.getReceivedPackageCount() == null ? 0 : tail.getReceivedPackageCount(), packageCount));
|
||||
tail.setProductQuantity(Math.addExact(tail.getProductQuantity() == null ? 0 : tail.getProductQuantity(), Math.toIntExact(allocations.stream().mapToLong(Allocation::quantity).sum())));
|
||||
if ("PALLET".equals(receipt.getUnitType())) tail.setPalletCount(Math.addExact(tail.getPalletCount() == null ? 0 : tail.getPalletCount(), 1));
|
||||
if (receipt.getLength() != null && receipt.getWidth() != null && receipt.getHeight() != null) {
|
||||
tail.setVolumeCbm(receipt.getLength().multiply(receipt.getWidth()).multiply(receipt.getHeight()).divide(java.math.BigDecimal.valueOf(1_000_000)));
|
||||
// Recalculate from physical carriers; signing another pallet must not overwrite the first pallet's volume.
|
||||
Map<String, ReturnReceipt> measuredUnits = new LinkedHashMap<>();
|
||||
List<ReturnReceipt> signedReceipts = new ArrayList<>(receipts.listOrder(order.getId()));
|
||||
if (signedReceipts.stream().noneMatch(line -> Objects.equals(line.getId(), receipt.getId()))) signedReceipts.add(0, receipt);
|
||||
for (ReturnReceipt signed : signedReceipts) {
|
||||
if (signed.getLength() == null || signed.getWidth() == null || signed.getHeight() == null || "PARCEL".equals(signed.getUnitType())) continue;
|
||||
String code = text(signed.getUnitCode()) == null ? signed.getReceiptNo() : signed.getUnitCode();
|
||||
measuredUnits.putIfAbsent(signed.getUnitType() + ":" + code, signed);
|
||||
}
|
||||
tail.setVolumeCbm(measuredUnits.values().stream().map(line -> line.getLength().multiply(line.getWidth()).multiply(line.getHeight())
|
||||
.divide(java.math.BigDecimal.valueOf(1_000_000))).reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add));
|
||||
tail.setActualArrivalAt(tail.getActualArrivalAt() == null ? now : tail.getActualArrivalAt());
|
||||
tail.setUpdatedAt(now); lastlegOrders.updateById(tail);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public class LastlegOrderService {
|
||||
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||||
@Resource private WarehouseResourceGateway warehouseResourceGateway;
|
||||
@Resource private ContainerShippingGateMapper containerShippingGates;
|
||||
@Resource private ReturnOrderReferenceResolver returnOrderReferences;
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
return page(customerId, query, null);
|
||||
@@ -93,6 +94,8 @@ public class LastlegOrderService {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
new Page<>(query.page(), query.size()), filter(customerId, query, relabelNo).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId));
|
||||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
|
||||
returnOrderReferences.populateForRead(page.getRecords());
|
||||
refreshReturnVolumes(page.getRecords());
|
||||
Map<String, String> containerNodes = containerNodes(page.getRecords());
|
||||
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
|
||||
Map<Long, LastlegOrderMapper.OrderWeightRow> weights = orderWeights(page.getRecords());
|
||||
@@ -331,6 +334,8 @@ public class LastlegOrderService {
|
||||
|
||||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||||
LastlegOrder order = findOwned(customerId, orderId);
|
||||
returnOrderReferences.populateForRead(List.of(order));
|
||||
refreshReturnVolumes(List.of(order));
|
||||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
|
||||
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
|
||||
@@ -596,6 +601,14 @@ public class LastlegOrderService {
|
||||
LastlegOrderMapper.DispatchStatusRow::dispatchStatus));
|
||||
}
|
||||
|
||||
private void refreshReturnVolumes(List<LastlegOrder> orders) {
|
||||
List<Long> ids = orders.stream().filter(order -> "RETURN_RELABEL".equals(order.getOrderType())).map(LastlegOrder::getId).toList();
|
||||
if (ids.isEmpty()) return;
|
||||
Map<Long, BigDecimal> volumes = orderMapper.findReturnVolumes(ids).stream()
|
||||
.collect(Collectors.toMap(LastlegOrderMapper.ReturnVolumeRow::orderId, LastlegOrderMapper.ReturnVolumeRow::volumeCbm));
|
||||
for (LastlegOrder order : orders) if (volumes.containsKey(order.getId())) order.setVolumeCbm(volumes.get(order.getId()));
|
||||
}
|
||||
|
||||
private Map<Long, LastlegOrderMapper.OrderWeightRow> orderWeights(List<LastlegOrder> orders) {
|
||||
if (orders.isEmpty()) return Map.of();
|
||||
List<LastlegOrderMapper.OrderWeightRow> rows = orderMapper.findOrderWeights(
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Return snapshots come from the owning services, never another service's database. */
|
||||
@Service
|
||||
public class ReturnOrderReferenceResolver {
|
||||
private static final Logger log = LoggerFactory.getLogger(ReturnOrderReferenceResolver.class);
|
||||
private final FleetCustomerDirectory customers;
|
||||
private final WarehouseResourceGateway warehouses;
|
||||
|
||||
public ReturnOrderReferenceResolver(FleetCustomerDirectory customers, WarehouseResourceGateway warehouses) {
|
||||
this.customers = customers;
|
||||
this.warehouses = warehouses;
|
||||
}
|
||||
|
||||
public void populate(List<LastlegOrder> orders) {
|
||||
populate(orders, true);
|
||||
}
|
||||
|
||||
public void populateForRead(List<LastlegOrder> orders) {
|
||||
populate(orders, false);
|
||||
}
|
||||
|
||||
private void populate(List<LastlegOrder> orders, boolean required) {
|
||||
Map<String, FleetCustomerDirectory.Customer> customerReferences = new HashMap<>();
|
||||
Map<String, WarehouseResourceGateway.WarehouseReference> warehouseReferences = new HashMap<>();
|
||||
for (LastlegOrder order : orders) {
|
||||
if (!"RETURN_RELABEL".equals(order.getOrderType())) continue;
|
||||
if (!StringUtils.hasText(order.getCustomerName())) {
|
||||
try {
|
||||
var customer = customerReferences.computeIfAbsent(order.getCustomerId(), customers::find);
|
||||
order.setCustomerName(customer.name());
|
||||
} catch (ResponseStatusException error) {
|
||||
if (required) throw error;
|
||||
log.warn("Return customer reference unavailable for order {}, status {}", order.getId(), error.getStatusCode().value());
|
||||
}
|
||||
}
|
||||
if (!StringUtils.hasText(order.getOverseasWarehouseCode()) || !StringUtils.hasText(order.getOverseasWarehouseName())) {
|
||||
try {
|
||||
var warehouse = warehouseReferences.computeIfAbsent(order.getOverseasWarehouseId(),
|
||||
id -> warehouses.requireActiveSharedWarehouse(Long.parseLong(id)));
|
||||
order.setOverseasWarehouseCode(warehouse.warehouseCode());
|
||||
order.setOverseasWarehouseName(warehouse.warehouseName());
|
||||
} catch (ResponseStatusException error) {
|
||||
if (required) throw error;
|
||||
log.warn("Return warehouse reference unavailable for order {}, status {}", order.getId(), error.getStatusCode().value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -895,6 +895,30 @@ class RelabelApplicationServiceTest {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnSkuRelabelRejectsQuantityBeyondSelectedSourceBox() {
|
||||
var cartonMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartonMapper);
|
||||
ReflectionTestUtils.setField(service, "returnCartonItems", lineMapper);
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setId(901L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1");
|
||||
carton.setCartonMark("RETURN-BOX-1"); carton.setStatus("AVAILABLE"); carton.setSourceUnitType("BOX");
|
||||
var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
line.setSkuCode("SKU-A"); line.setQuantity(5L); line.setPackingReservedQuantity(0L);
|
||||
when(cartonMapper.lockId(901L)).thenReturn(carton);
|
||||
when(lineMapper.lockCarton(901L)).thenReturn(List.of(line));
|
||||
var detail = new RelabelRequests.Item(null, null, null, "SKU-A", null, null, null,
|
||||
"Return goods", 1, 1, "RT-1", null, "NEW-A", null, null,
|
||||
null, null, null, "RETURN-BOX-1", null, null, "SKU_LABEL",
|
||||
List.of(new RelabelRequests.SkuItem("SKU-A", "NEW-A", 6, null)), 901L);
|
||||
var request = new RelabelRequests.Create("RT-1", "WH-1", LocalDateTime.now().plusDays(1),
|
||||
"Li", "13800000000", null, null, "return-over-quantity", List.of(detail), null, false);
|
||||
var error = assertThrows(ResponseStatusException.class, () -> service.create("10001", "operator-1", request));
|
||||
assertTrue(error.getReason().contains("超过来源箱可用数量"));
|
||||
verify(applications, never()).insert(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
private void stubDetail(RelabelApplication application) {
|
||||
when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6)));
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.lastleg.returnreceipt.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class MerchantReturnOrderControllerTest {
|
||||
@Test
|
||||
void putRoutesEditedForecastToAuthenticatedCustomer() throws Exception {
|
||||
var service = mock(ReturnReceiptService.class);
|
||||
var controller = new MerchantReturnOrderController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
var user = new AuthenticatedUser(1L, 100L, 100L, 1L, "tester", "MERCHANT", "WEB", Set.of());
|
||||
var mvc = MockMvcBuilders.standaloneSetup(controller).setCustomArgumentResolvers(new HandlerMethodArgumentResolver() {
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType() == AuthenticatedUser.class;
|
||||
}
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
|
||||
NativeWebRequest request, WebDataBinderFactory factory) {
|
||||
return user;
|
||||
}
|
||||
}).build();
|
||||
mvc.perform(put("/api/merchant/v1/last-leg/return-orders/RT-TEST")
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{"overseasWarehouseId":"WH-1","warehouseContactName":"Updated contact",
|
||||
"warehouseContactPhone":"123456","customerReferenceNo":"REF-NEW",
|
||||
"needRelabel":true,"remark":"Updated remark","idempotencyKey":"edit-test",
|
||||
"items":[{"skuCode":"SKU-NEW","productNameZh":"Updated product","expectedQuantity":25}]}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
verify(service).updatePending(eq("100"), eq("RT-TEST"), eq(new ReturnOrderRequests.Create(
|
||||
"WH-1", "Updated contact", "123456", "REF-NEW", true, "Updated remark", "edit-test",
|
||||
java.util.List.of(new ReturnOrderRequests.Item("SKU-NEW", "Updated product", 25)))));
|
||||
verifyNoMoreInteractions(service);
|
||||
}
|
||||
}
|
||||
+28
@@ -70,6 +70,7 @@ class ReturnReceiptServiceTest {
|
||||
@Mock RelabelItemMapper relabelItems;
|
||||
@Mock FbaWarehouseValidator fbaWarehouseValidator;
|
||||
@Mock RelabelPhotoFileGateway photoFiles;
|
||||
@Mock com.baiyue.lastleg.service.ReturnOrderReferenceResolver returnOrderReferences;
|
||||
|
||||
ReturnReceiptService service;
|
||||
PdaWarehouseIdentity identity;
|
||||
@@ -91,6 +92,7 @@ class ReturnReceiptServiceTest {
|
||||
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
|
||||
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
|
||||
ReflectionTestUtils.setField(service, "fbaWarehouseValidator", fbaWarehouseValidator);
|
||||
ReflectionTestUtils.setField(service, "returnOrderReferences", returnOrderReferences);
|
||||
identity = new PdaWarehouseIdentity("operator-1", "仓管员", 10L);
|
||||
}
|
||||
|
||||
@@ -110,6 +112,32 @@ class ReturnReceiptServiceTest {
|
||||
assertEquals(90L, result.receivedQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsMeasurementsForBothPalletsAndDoesNotAddRepeatedCarrierVolume() {
|
||||
ReturnOrder order = order("RT-TWO-PALLETS", "10", "COMPLETED", 10, 10);
|
||||
when(orders.findNo("customer-1", "RT-TWO-PALLETS")).thenReturn(order);
|
||||
ReturnReceipt first = measuredReceipt(1L, "PLT-A", "111", "11", "12");
|
||||
ReturnReceipt second = measuredReceipt(2L, "PLT-B", "11", "21", "32");
|
||||
ReturnReceipt earlier = measuredReceipt(3L, "PLT-A", "100", "10", "10");
|
||||
when(receipts.listOrder(order.getId())).thenReturn(List.of(second, first, earlier));
|
||||
var result = service.detail("customer-1", "RT-TWO-PALLETS");
|
||||
assertEquals(2, result.pallets().size());
|
||||
var palletA = result.pallets().stream().filter(row -> "PLT-A".equals(row.palletCode())).findFirst().orElseThrow();
|
||||
var palletB = result.pallets().stream().filter(row -> "PLT-B".equals(row.palletCode())).findFirst().orElseThrow();
|
||||
assertThat(palletA.volumeCbm()).isEqualByComparingTo("0.014652");
|
||||
assertThat(palletB.volumeCbm()).isEqualByComparingTo("0.007392");
|
||||
assertThat(palletA.length()).isEqualByComparingTo("111");
|
||||
assertThat(palletB.weight()).isEqualByComparingTo("11");
|
||||
}
|
||||
|
||||
private ReturnReceipt measuredReceipt(long id, String code, String length, String width, String height) {
|
||||
ReturnReceipt receipt = new ReturnReceipt(); receipt.setId(id); receipt.setUnitType("PALLET"); receipt.setUnitCode(code);
|
||||
receipt.setPalletCode(code); receipt.setTotalBoxCount(2L); receipt.setReceiptNo("RR-" + id);
|
||||
receipt.setLength(new java.math.BigDecimal(length)); receipt.setWidth(new java.math.BigDecimal(width));
|
||||
receipt.setHeight(new java.math.BigDecimal(height)); receipt.setWeight(new java.math.BigDecimal("11"));
|
||||
return receipt;
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantPhotoDownloadUsesAuthenticatedCustomerAndResolvedReturnOrder() {
|
||||
ReflectionTestUtils.setField(service, "photoFiles", photoFiles);
|
||||
|
||||
@@ -74,6 +74,7 @@ class LastlegOrderServiceTest {
|
||||
@Mock ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||||
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
||||
@Mock ContainerShippingGateMapper containerShippingGates;
|
||||
@Mock ReturnOrderReferenceResolver returnOrderReferences;
|
||||
LastlegOrderService service;
|
||||
|
||||
@Test
|
||||
@@ -105,7 +106,10 @@ class LastlegOrderServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(
|
||||
new org.apache.ibatis.builder.MapperBuilderAssistant(new org.apache.ibatis.session.Configuration(), ""), LastlegOrder.class);
|
||||
service = new LastlegOrderService();
|
||||
ReflectionTestUtils.setField(service, "returnOrderReferences", returnOrderReferences);
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orders);
|
||||
ReflectionTestUtils.setField(service, "detailMapper", details);
|
||||
ReflectionTestUtils.setField(service, "historyMapper", histories);
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ReturnOrderReferenceResolverTest {
|
||||
@Test
|
||||
void directoryFailureBlocksNewSnapshotButDoesNotHideHistoricalOrders() {
|
||||
var customers = mock(FleetCustomerDirectory.class);
|
||||
var warehouses = mock(WarehouseResourceGateway.class);
|
||||
when(customers.find("1")).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE));
|
||||
when(warehouses.requireActiveSharedWarehouse(10L)).thenReturn(new WarehouseResourceGateway.WarehouseReference(10L, "USLAX01", "洛杉矶仓", "ACTIVE"));
|
||||
var resolver = new ReturnOrderReferenceResolver(customers, warehouses);
|
||||
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> resolver.populate(List.of(order())));
|
||||
var historical = order();
|
||||
resolver.populateForRead(List.of(historical));
|
||||
assertEquals("洛杉矶仓", historical.getOverseasWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesMissingReturnReferencesOncePerCustomerAndWarehouseAndPreservesSnapshots() {
|
||||
var customers = mock(FleetCustomerDirectory.class);
|
||||
var warehouses = mock(WarehouseResourceGateway.class);
|
||||
when(customers.find("1")).thenReturn(new FleetCustomerDirectory.Customer("1", "C1", "测试货主", null, null));
|
||||
when(warehouses.requireActiveSharedWarehouse(10L)).thenReturn(new WarehouseResourceGateway.WarehouseReference(10L, "USLAX01", "洛杉矶仓", "ACTIVE"));
|
||||
LastlegOrder first = order(), second = order(), frozen = order();
|
||||
frozen.setCustomerName("原货主快照"); frozen.setOverseasWarehouseCode("OLD"); frozen.setOverseasWarehouseName("原仓库快照");
|
||||
|
||||
new ReturnOrderReferenceResolver(customers, warehouses).populate(List.of(first, second, frozen));
|
||||
|
||||
assertEquals("测试货主", first.getCustomerName());
|
||||
assertEquals("USLAX01", second.getOverseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", second.getOverseasWarehouseName());
|
||||
assertEquals("原货主快照", frozen.getCustomerName());
|
||||
assertEquals("原仓库快照", frozen.getOverseasWarehouseName());
|
||||
verify(customers, times(1)).find("1");
|
||||
verify(warehouses, times(1)).requireActiveSharedWarehouse(10L);
|
||||
}
|
||||
|
||||
private LastlegOrder order() {
|
||||
var order = new LastlegOrder(); order.setOrderType("RETURN_RELABEL");
|
||||
order.setCustomerId("1"); order.setOverseasWarehouseId("10"); return order;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user