diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java index 7854f98b1..462e0ce41 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java @@ -44,7 +44,8 @@ public final class RelabelResponses { String sourceType, Long returnCartonId, String cartonMark, List skuItems, long relabelledPackages, String relabelStatus, long relabelledQuantity, long relabelReservedQuantity, Long stockQuantity, String inventoryStatus, String occupiedRelabelNo, List mergedIntoCartonMarks, boolean hasOutbound, - List skuConversions) { + List skuConversions, + List cartonSkuItems, long cartonAvailablePackages) { public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode, String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName, String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh, @@ -53,7 +54,7 @@ public final class RelabelResponses { this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName, skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark, - skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false, List.of()); + skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false, List.of(), List.of(), 0); } } public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index 6e72bba6d..8f69deb39 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -284,7 +284,13 @@ public class RelabelApplicationService implements WalletResultHandler { return new RelabelResponses.SkuItem(item.getSkuCode(), null, Math.toIntExact(Math.max(remaining, 0)), null); }).filter(item -> !"AVAILABLE".equals(carton.getStatus()) || item.quantity() > 0).toList(); - if (cartonSkus.isEmpty() && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue; + List physicalSkus = cartonLines.stream() + .filter(line -> value(line.getQuantity()) > 0) + .map(line -> new RelabelResponses.SkuItem(line.getSkuCode(), null, Math.toIntExact(value(line.getQuantity())), null)).toList(); + long cartonAvailable = "AVAILABLE".equals(carton.getStatus()) && carton.getRelabelApplicationId() == null + && cartonLines.stream().noneMatch(line -> value(line.getPackingReservedQuantity()) > 0) + && !physicalSkus.isEmpty() ? 1 : 0; + if (cartonSkus.isEmpty() && cartonAvailable == 0 && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue; String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode) .collect(Collectors.joining(",")); long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum(); @@ -301,7 +307,7 @@ public class RelabelApplicationService implements WalletResultHandler { .filter(history -> carton.getCartonMark().equals(history.sourceCartonMark())) .map(history -> history.targetCartonMark()).distinct().toList(), returnCartons.hasOutbound(carton.getId()), skuConversionRows.listCarton(customerId, carton.getId()).stream() - .map(com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion::from).toList())); + .map(com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion::from).toList(), physicalSkus, cartonAvailable)); } return result; } @@ -414,7 +420,8 @@ public class RelabelApplicationService implements WalletResultHandler { final long sourceReturnCartonId = carton.getId(); Map availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream() .collect(Collectors.toMap(line -> line.getSkuCode().toUpperCase(java.util.Locale.ROOT), - line -> remainingRelabelQuantity(sourceReturnCartonId, line), Long::sum)); + line -> "CARTON_LABEL".equals(operationType(item)) ? value(line.getQuantity()) + : remainingRelabelQuantity(sourceReturnCartonId, line), Long::sum)); if (availableSku.values().stream().noneMatch(quantity -> quantity > 0)) throw conflict("来源箱 SKU 已全部换标,没有剩余可换标数量"); Map requestedSku = selectedReturnSkus.computeIfAbsent(carton.getId(), ignored -> new java.util.HashMap<>()); diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 8d5571c86..1fc755f60 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -3110,6 +3110,15 @@ components: RelabelInventoryCandidate: type: object properties: + cartonAvailablePackages: { type: integer, format: int64, minimum: 0, maximum: 1, description: 仅换箱唛可选择的当前实物箱数;已换 SKU 不影响可选箱数,占用和合箱预留仍不可选 } + cartonSkuItems: + type: array + description: 当前实物箱内全部 SKU 和件数,包含换标后的新 SKU + items: + type: object + properties: + skuCode: { type: string } + quantity: { type: integer, minimum: 1 } skuConversions: { type: array, items: { $ref: '#/components/schemas/ReturnSkuConversion' }, description: 当前实物关联的旧新SKU转换、实际件数和客户确认状态 } relabelStatus: { type: string, nullable: true, enum: [UNLABELLED, PROCESSING, PARTIALLY_COMPLETED, COMPLETED], description: 退货实物按实际完成件数和未完成申请占用计算 } relabelledPackages: { type: integer, format: int64, description: 退货实际已完成商品换标件数,不从可用库存差值推导 } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java index 20608ec9d..2dea60a16 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java @@ -109,11 +109,35 @@ class RelabelApplicationServiceTest { assertEquals(4L, candidate.stockQuantity()); assertEquals("AVAILABLE", candidate.inventoryStatus()); assertEquals(List.of(), candidate.mergedIntoCartonMarks()); + assertEquals(1, candidate.cartonAvailablePackages()); carton.setStatus("RELABEL_PENDING"); var occupied = service.candidates("C", "W", null).get(0); assertEquals(0, occupied.availablePackages()); + assertEquals(0, occupied.cartonAvailablePackages()); assertEquals(2, occupied.skuItems().size(), "处理中库存保留占用信息,但不可再次选择"); } + @Test void convertedSkuRemainsAvailableForCartonLabelOnly() { + var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class); + var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class); + ReflectionTestUtils.setField(service, "returnCartons", cartons); + ReflectionTestUtils.setField(service, "returnCartonItems", lines); + ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class)); + var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); + carton.setId(20L); carton.setStatus("AVAILABLE"); carton.setCartonMark("NEW-BOX"); carton.setTotalQuantity(15L); + var first = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); + first.setSkuCode("NEW-A"); first.setQuantity(10L); first.setRelabelledQuantity(10L); + var second = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); + second.setSkuCode("NEW-B"); second.setQuantity(5L); second.setRelabelledQuantity(5L); + when(cartons.findRelabelCandidates("C", "W")).thenReturn(List.of(carton)); + when(lines.listCarton(20L)).thenReturn(List.of(first, second)); + var candidate = service.candidates("C", "W", null).get(0); + assertEquals(0, candidate.availablePackages()); + assertTrue(candidate.skuItems().isEmpty()); + assertEquals("NEW-BOX", candidate.cartonMark()); + assertEquals(1, candidate.cartonAvailablePackages()); + assertEquals(List.of(new RelabelResponses.SkuItem("NEW-A", null, 10, null), + new RelabelResponses.SkuItem("NEW-B", null, 5, null)), candidate.cartonSkuItems()); + } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(longs = {0, 2, 5, 8}) void returnCandidatesOnlyOfferUnlabelledPieces(long completed) { @@ -1066,7 +1090,7 @@ class RelabelApplicationServiceTest { } @org.junit.jupiter.params.ParameterizedTest - @org.junit.jupiter.params.provider.CsvSource({"CARTON_LABEL,0", "SKU_LABEL,0", "SKU_LABEL,2", "CARTON_AND_SKU,0"}) + @org.junit.jupiter.params.provider.CsvSource({"CARTON_LABEL,0", "CARTON_LABEL,5", "SKU_LABEL,0", "SKU_LABEL,2", "CARTON_AND_SKU,0"}) void intactParcelSupportsAllLabelOperationsWithoutMergingIntoBox(String type, long completed) { var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class); var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class); @@ -1077,7 +1101,8 @@ class RelabelApplicationServiceTest { parcel.setCountStatus("CONFIRMED"); parcel.setTotalQuantity(5L); var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(5L); when(cartons.lockId(901L)).thenReturn(parcel); when(lines.lockCarton(901L)).thenReturn(List.of(line)); - when(cartons.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed); + if ("CARTON_LABEL".equals(type)) line.setRelabelledQuantity(completed); + else when(cartons.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed); when(cartons.markRelabelPending(anyLong(), anyLong(), anyString(), any())).thenReturn(1); var app = application(11L, "RL-PARCEL", "10001", "parcel-key", null, "PENDING_REVIEW"); app.setLastlegOrderId(21L); when(applications.findNo(eq("10001"), any())).thenReturn(app);