贴标申请,仓储转运

This commit is contained in:
2026-09-20 15:06:17 +08:00
parent a7d458cddd
commit 02844da360
56 changed files with 2121 additions and 111 deletions
@@ -66,6 +66,40 @@ class DeliveryCompletedOutboxServiceTest {
verifyNoInteractions(mapper);
}
@Test
void skipsWarehouseTransferWithoutDomesticSourceOrder() {
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
DeliveryCompletedOutboxService service = new DeliveryCompletedOutboxService();
ReflectionTestUtils.setField(service, "outbox", mapper);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().registerModule(new JavaTimeModule()));
DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(2L); delivery.setDeliveryNo("DL-WT"); delivery.setVersion(1L);
LastlegOrder transfer = new LastlegOrder();
transfer.setId(22L); transfer.setOrderType("WAREHOUSE_TRANSFER");
transfer.setCustomerId("88"); transfer.setSourceInboundId("WAREHOUSE_TRANSFER:WT-1");
service.publish(delivery, List.of(transfer), Instant.parse("2026-09-15T08:00:00Z"));
verifyNoInteractions(mapper);
}
@Test
void skipsOverseasLocalOrderWithoutDomesticSourceOrder() {
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
DeliveryCompletedOutboxService service = new DeliveryCompletedOutboxService();
ReflectionTestUtils.setField(service, "outbox", mapper);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().registerModule(new JavaTimeModule()));
DeliveryOrder delivery = new DeliveryOrder();
delivery.setId(3L); delivery.setDeliveryNo("DL-LOCAL"); delivery.setVersion(1L);
LastlegOrder local = new LastlegOrder();
local.setId(23L); local.setOrderType("RETURN_RELABEL"); local.setSourceRegion("CUSTOMER");
local.setCustomerId("88"); local.setSourceInboundId("RETURN-1");
service.publish(delivery, List.of(local), Instant.parse("2026-09-15T08:00:00Z"));
verifyNoInteractions(mapper);
}
private LastlegOrder order(long id, String inboundId, String orderId, String inboundCode, String fba) {
LastlegOrder value = new LastlegOrder();
value.setId(id); value.setOrderNo("LM-" + id); value.setCustomerId("88");
@@ -1,6 +1,8 @@
package com.baiyue.lastleg.palletops.controller;
import com.baiyue.lastleg.palletops.dto.PalletOperationResponses;
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -8,6 +10,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
@@ -35,4 +38,21 @@ class PdaPalletOperationControllerContractTest {
assertFalse(sql.contains("<"));
assertFalse(sql.contains(">"));
}
@Test void relabelSourceAndResolvedPalletSerializeSnowflakeIdsAsStrings() throws Exception {
ObjectMapper mapper = new ObjectMapper();
var source = new PalletOperationResponses.RelabelSource(2100204001227157507L, 2L,
2100203890682015746L, "PLT-1", "LOC-1", "IN-1", 1L, 1L, List.of("C-1"));
var content = new PalletOperationResponses.PalletContent(2100203890682015746L, "PLT-1",
"OCCUPIED", 1, 2092783373163335682L, "LOC-1", null, 1L, List.of());
assertEquals("2100204001227157507", mapper.readTree(mapper.writeValueAsString(source))
.path("positionId").textValue());
assertEquals("2100203890682015746", mapper.readTree(mapper.writeValueAsString(source))
.path("palletId").textValue());
assertEquals("2100203890682015746", mapper.readTree(mapper.writeValueAsString(content))
.path("palletId").textValue());
assertEquals("2092783373163335682", mapper.readTree(mapper.writeValueAsString(content))
.path("locationId").textValue());
}
}
@@ -9,6 +9,7 @@ import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@@ -37,4 +38,23 @@ class HttpPalletPhysicalGatewayTest {
.hasMessageContaining("404");
server.verify();
}
@Test
void resolveIgnoresNewWarehousePalletMetadata() {
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-1"))
.andRespond(withSuccess("""
{"code":"200","message":"success","data":{"palletId":"30","palletCode":"PAL-1",
"warehouseId":"88","locationId":"40","status":"IDLE","fbaWarehouseCode":"XLX1",
"unitPrice":1.00,"version":2,"locationType":"VIRTUAL","locationStatus":"ACTIVE",
"reservable":true,"reservationBlockReason":null},
"timestamp":"2026-09-19T03:18:35.283336500Z"}
""", MediaType.APPLICATION_JSON));
var result = gateway.resolve("PAL-1");
assertThat(result.palletId()).isEqualTo(30L);
assertThat(result.palletCode()).isEqualTo("PAL-1");
assertThat(result.fbaWarehouseCode()).isEqualTo("XLX1");
server.verify();
}
}
@@ -18,6 +18,11 @@ import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.service.RelabelPalletMutationService;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryBalanceMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -62,6 +67,9 @@ class PdaPalletOperationServiceTest {
@Mock private RelabelApplicationMapper relabelApplications;
@Mock private RelabelItemMapper relabelItems;
@Mock private RelabelPalletMutationService relabelPalletMutations;
@Mock private ReturnInventoryCartonMapper returnCartons;
@Mock private ReturnInventoryCartonItemMapper returnCartonItems;
@Mock private ReturnInventoryBalanceMapper returnBalances;
private PdaPalletOperationService service;
@BeforeEach void setUp() {
@@ -75,10 +83,44 @@ class PdaPalletOperationServiceTest {
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "relabelPalletMutations", relabelPalletMutations);
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
ReflectionTestUtils.setField(service, "returnCartonItems", returnCartonItems);
ReflectionTestUtils.setField(service, "returnBalances", returnBalances);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
service.configureTransactions(new NoOpTransactionManager());
}
@Test void partialReturnRelabelKeepsUnlabelledRemainderOnOriginalPallet() {
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100"); app.setRelabelNo("RL-1");
RelabelItem item = new RelabelItem(); item.setId(701L); item.setSkuCode("SKU-1");
item.setReturnCartonId(801L); item.setDestinationFbaWarehouseCode("XLX6");
ReturnInventoryCarton original = new ReturnInventoryCarton(); original.setId(801L);
original.setCustomerId("100"); original.setOverseasWarehouseId("88");
original.setTotalQuantity(100L); original.setPalletCode("OLD");
ReturnInventoryCartonItem originalLine = new ReturnInventoryCartonItem();
originalLine.setId(901L); originalLine.setSkuCode("SKU-1"); originalLine.setQuantity(100L);
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(returnCartonItems.lockCarton(801L)).thenReturn(List.of(originalLine));
when(returnCartonItems.updateById(any(ReturnInventoryCartonItem.class))).thenReturn(1);
when(returnCartons.releasePartial(801L, "100", 600L, 70L)).thenReturn(1);
when(returnCartons.insert(any(ReturnInventoryCarton.class))).thenReturn(1);
when(returnCartonItems.insert(any(ReturnInventoryCartonItem.class))).thenReturn(1);
when(returnBalances.adjustCartonQuantity("100", "88", "SKU-1", 1)).thenReturn(1);
when(relabelItems.updateById(any(RelabelItem.class))).thenReturn(1);
ReflectionTestUtils.invokeMethod(service, "splitReturnRelabelCarton", app, item, original,
pallet(30L, "NEW-XLX6", 88L, 0), 30L);
assertEquals(70L, originalLine.getQuantity());
var moved = ArgumentCaptor.forClass(ReturnInventoryCarton.class);
verify(returnCartons).insert(moved.capture());
assertEquals(30L, moved.getValue().getTotalQuantity());
assertEquals("NEW-XLX6", moved.getValue().getPalletCode());
assertEquals("XLX6", moved.getValue().getDestinationWarehouseCode());
assertEquals(moved.getValue().getId(), item.getReturnCartonId());
verify(returnBalances).adjustCartonQuantity("100", "88", "SKU-1", 1);
}
@Test void resolveRejectsPalletOutsideTokenFrozenWarehouse() {
when(physical.resolve("P-1")).thenReturn(pallet(10L, "P-1", 99L, 1));
assertThrows(ResponseStatusException.class, () -> service.resolve(identity(),
@@ -100,7 +142,7 @@ class PdaPalletOperationServiceTest {
RelabelApplication application = new RelabelApplication(); application.setRelabelNo("RL-1");
application.setStatus("READY_FOR_OUTBOUND"); application.setOverseasWarehouseId("88");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(application);
when(physical.ensureRelabelPallet(88L, "RL-1:new-pallet-1")).thenReturn(
when(physical.ensureRelabelPallet(88L, "RL-1:new-pallet-1", null)).thenReturn(
new PalletPhysicalGateway.PalletReference(30L, "PAL-NEW-1", 88L, 1030L, "IDLE", null, 0));
var result = service.createRelabelPallet(identity(),
@@ -358,6 +400,27 @@ class PdaPalletOperationServiceTest {
assertEquals(java.util.Set.of(), ReflectionTestUtils.invokeMethod(service, "boundRelabelMarks", app));
}
@Test void relabelSourcesExposeAuthoritativePalletId() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
app.setRelabelNo("RL-1"); app.setOverseasWarehouseId("88"); app.setStatus("READY_FOR_OUTBOUND");
app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
RelabelItem item = relabelItem(701L, "CARTON-A", 100L);
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(positions.selectById(100L)).thenReturn(position(100L, 20L, "DISPLAY-CODE", 1L, 3L));
PalletInventoryRow source = row(100L, 20L, 3L, 0L); source.setPalletNo("CANONICAL-CODE");
source.setHoldPackages(1L);
when(mapper.relabelSources("100", "88", 200L)).thenReturn(List.of(source));
var result = service.relabelSources(identity(), new PalletOperationRequests.RelabelSources("RL-1"));
assertEquals(1, result.size());
assertEquals(20L, result.get(0).palletId());
assertEquals("CANONICAL-CODE", result.get(0).palletCode());
}
@Test void createsPalletBeforeCustomerConfirmationOnlyForClaimingOperator() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
@@ -366,7 +429,7 @@ class PdaPalletOperationServiceTest {
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setStatus("PROCESSING"); task.setOperatorKey("7");
when(tasks.find("100", 600L)).thenReturn(task);
when(physical.ensureRelabelPallet(88L, "RL-1:before-confirm")).thenReturn(pallet(30L, "NEW", 88L, 0));
when(physical.ensureRelabelPallet(88L, "RL-1:before-confirm", null)).thenReturn(pallet(30L, "NEW", 88L, 0));
assertEquals("NEW", service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "before-confirm")).palletCode());
task.setOperatorKey("other");
assertThrows(ResponseStatusException.class, () -> service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "different")));
@@ -117,4 +117,20 @@ class RelabelContractTest {
assertTrue(service.contains("requiresOutbound && (!StringUtils.hasText(deliveryMethod)"
+ " || !DELIVERY_METHODS.contains(deliveryMethod))"));
}
@Test
void returnRelabelReviewStatusIsProjectedWithoutLosingPendingOrRejectedState() throws Exception {
String service = Files.readString(Path.of("src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java"),
StandardCharsets.UTF_8);
assertTrue(service.contains("Set.of(PENDING_REVIEW, PENDING_PROCESSING, \"PROCESSING\", \"REJECTED\", \"CANCELLED\")"));
assertTrue(service.contains("return workflowStatus;"));
}
@Test
void returnCartonConstraintsAllowRelabelReservationWithPositiveInventory() throws Exception {
String baseline = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8)
.replaceAll("\\s+", "").toUpperCase();
assertTrue(baseline.contains("STATUSIN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED')ANDSKU_TYPES>0ANDTOTAL_QUANTITY>0"));
assertTrue(baseline.contains("STATUSIN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED')"));
}
}
@@ -264,6 +264,73 @@ class PdaRelabelServiceTest {
verify(unitFacts, never()).insert(any(PdaRelabelUnitFact.class));
}
@Test
void returnSkuTotalCompletesOneSkuOnceAndAllocatesAcrossSourcePalletItems() {
application.setStatus("PROCESSING"); application.setNotifiedPackageCount(100);
item.setOperationType("RETURN_BULK_SKU_LABEL"); item.setSkuCode("sku-fba100");
item.setNotifiedPackageCount(50); item.setCartonMark("PALLET-A");
RelabelItem second = item(); second.setId(11L); second.setOperationType("RETURN_BULK_SKU_LABEL");
second.setSkuCode("sku-fba100"); second.setNotifiedPackageCount(50); second.setCartonMark("PALLET-B");
PdaRelabelTask task = task("PROCESSING", 0); task.setExpectedPackages(100);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
when(items.selectList(any())).thenReturn(List.of(item, second));
var result = service.completeReturnSkuTotal("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.BulkSkuTotal("sku-fba100", 100, 0, "sku-total-1"));
assertEquals(100, result.relabelledPackages());
assertEquals(2, persistedFacts.size());
assertEquals(List.of(50, 50), persistedFacts.stream().map(PdaRelabelUnitFact::getQuantity).toList());
assertEquals(List.of(10L, 11L), persistedFacts.stream().map(PdaRelabelUnitFact::getItemId).toList());
assertTrue(persistedFacts.stream().allMatch(fact -> fact.getOldLabel() == null));
assertEquals(1, result.version());
}
@Test
void multiSkuPartialReturnRelabelRequiresBothSkuTotalsBeforeCompletion() {
verifyMultiSkuReturnCompletion(30, 15);
}
@Test
void multiSkuFullReturnRelabelRequiresBothSkuTotalsBeforeCompletion() {
verifyMultiSkuReturnCompletion(50, 40);
}
private void verifyMultiSkuReturnCompletion(int firstQuantity, int secondQuantity) {
application.setStatus("PROCESSING");
application.setNotifiedPackageCount(firstQuantity + secondQuantity);
item.setOperationType("RETURN_BULK_SKU_LABEL"); item.setSkuCode("SKU-A");
item.setNotifiedPackageCount(firstQuantity); item.setCartonMark("RC-A");
RelabelItem second = item(); second.setId(11L);
second.setOperationType("RETURN_BULK_SKU_LABEL"); second.setSkuCode("SKU-B");
second.setNotifiedPackageCount(secondQuantity); second.setCartonMark("RC-B");
PdaRelabelTask task = task("PROCESSING", 0);
task.setExpectedPackages(firstQuantity + secondQuantity);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
when(items.selectList(any())).thenReturn(List.of(item, second));
var first = service.completeReturnSkuTotal("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.BulkSkuTotal("SKU-A", firstQuantity, 0, "multi-a"));
assertEquals(firstQuantity, first.relabelledPackages());
assertThrows(ResponseStatusException.class, () -> service.complete("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Complete(List.of(501L), 1, "multi-early", null)));
assertThrows(ResponseStatusException.class, () -> service.completeReturnSkuTotal("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.BulkSkuTotal("SKU-A", 1, 1, "multi-over")));
var both = service.completeReturnSkuTotal("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.BulkSkuTotal("SKU-B", secondQuantity, 1, "multi-b"));
assertEquals(firstQuantity + secondQuantity, both.relabelledPackages());
assertEquals(List.of("SKU-A", "SKU-B"), persistedFacts.stream()
.map(fact -> fact.getItemId().equals(item.getId()) ? "SKU-A" : "SKU-B").toList());
assertEquals(List.of(firstQuantity, secondQuantity), persistedFacts.stream()
.map(PdaRelabelUnitFact::getQuantity).toList());
var completed = service.complete("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Complete(List.of(501L), 2, "multi-complete", "multi SKU complete"));
assertEquals(PdaRelabelService.WAIT_REVIEW, completed.taskStatus());
}
@Test
void completeRequiresEveryUnitAndMovesOnlyPdaTaskToWaitReview() {
application.setStatus("PROCESSING");
@@ -30,6 +30,8 @@ import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPdaTaskMapper;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -75,6 +77,7 @@ class RelabelApplicationServiceTest {
@Mock OutboundPdaTaskMapper outboundTasks;
@Mock WarehouseTransferOrderMapper warehouseTransfers;
@Mock OverseasFeeSourceService feeSources;
@Mock ReturnOrderMapper returnOrders;
@Mock com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper pdaTasks;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
@@ -103,6 +106,7 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
ReflectionTestUtils.setField(service, "outboundTasks", outboundTasks);
ReflectionTestUtils.setField(service, "warehouseTransfers", warehouseTransfers);
ReflectionTestUtils.setField(service, "returnOrders", returnOrders);
ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(walletCommands.publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class))).thenReturn("wallet-event-1");
lenient().when(inventoryPositions.findTenantPosition(any(), any())).thenAnswer(invocation -> {
@@ -411,6 +415,10 @@ class RelabelApplicationServiceTest {
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 1, 1)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order);
ReturnOrder returnOrder = new ReturnOrder();
returnOrder.setId(41L); returnOrder.setCustomerId("10001");
returnOrder.setRelabelApplicationId(application.getId()); returnOrder.setRelabelStatus("PROCESSING");
when(returnOrders.selectOne(any())).thenReturn(returnOrder);
service.changeStatus("10001", "FBR-REVIEW", "operator-2",
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, "submitted"));
@@ -419,6 +427,8 @@ class RelabelApplicationServiceTest {
assertEquals("WAIT_CUSTOMER_CONFIRMATION", application.getFulfillmentStatus());
assertEquals("COMPLETED", order.getRelabelStatus());
assertEquals("RELABEL:CUSTOMER_CONFIRM_PENDING", order.getSubOrderStatus());
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, returnOrder.getRelabelStatus());
verify(returnOrders).updateById(returnOrder);
}
@Test
@@ -558,6 +568,7 @@ class RelabelApplicationServiceTest {
new RelabelRequests.CustomerConfirm("confirm-transfer", "accepted"));
assertEquals("READY_FOR_DISPATCH", transfer.getStatus());
assertEquals("WAIT_PRE_DISPATCH", application.getFulfillmentStatus());
assertNotNull(transfer.getRelabelCompletedAt());
assertEquals("READY_FOR_DISPATCH", transferOrder.getCurrentStatus());
assertEquals("COMPLETED", transferOrder.getRelabelStatus());
@@ -610,6 +621,7 @@ class RelabelApplicationServiceTest {
assertEquals("ONT8", transfer.getFbaWarehouseCode());
assertEquals("READY_FOR_DISPATCH", transfer.getStatus());
assertEquals("WAIT_PRE_DISPATCH", application.getFulfillmentStatus());
assertEquals("ONT8", transferOrder.getFbaWarehouseCode());
assertEquals("READY_FOR_DISPATCH", transferOrder.getCurrentStatus());
}
@@ -45,6 +45,7 @@ class RelabelReturnCartonCandidateTest {
assertEquals("RETURN_CARTON", result.get(0).sourceType());
assertEquals(9001L, result.get(0).returnCartonId());
assertEquals("RC-WH-0001", result.get(0).cartonMark());
assertEquals(12, result.get(0).availablePackages());
assertEquals("SKU-RETURN", result.get(0).skuItems().get(0).skuCode());
assertEquals(12, result.get(0).skuItems().get(0).quantity());
}
@@ -4,6 +4,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.returnreceipt.dto.PdaReturnReceiptRequests;
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderRequests;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrderItem;
@@ -14,6 +15,14 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptBatchMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.service.FbaWarehouseValidator;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
@@ -53,6 +62,11 @@ class ReturnReceiptServiceTest {
@Mock ReturnInventoryService inventory;
@Mock LastlegOrderNoGenerator numbers;
@Mock LastlegOrderMapper lastlegOrders;
@Mock ReturnInventoryCartonMapper cartons;
@Mock ReturnInventoryCartonItemMapper cartonItems;
@Mock RelabelApplicationMapper relabelApplications;
@Mock RelabelItemMapper relabelItems;
@Mock FbaWarehouseValidator fbaWarehouseValidator;
ReturnReceiptService service;
PdaWarehouseIdentity identity;
@@ -69,9 +83,76 @@ class ReturnReceiptServiceTest {
ReflectionTestUtils.setField(service, "numbers", numbers);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "lastlegOrders", lastlegOrders);
ReflectionTestUtils.setField(service, "cartons", cartons);
ReflectionTestUtils.setField(service, "cartonItems", cartonItems);
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "fbaWarehouseValidator", fbaWarehouseValidator);
identity = new PdaWarehouseIdentity("operator-1", "仓管员", 10L);
}
@Test
void multiSkuPartialRelabelLocksOnlyRequestedQuantities() {
verifyMultiSkuRelabel(30, 15);
}
@Test
void multiSkuFullRelabelLocksAllReceivedQuantities() {
verifyMultiSkuRelabel(50, 40);
}
private void verifyMultiSkuRelabel(long firstQuantity, long secondQuantity) {
ReturnOrder order = order("RT-MULTI", "10", "COMPLETED", 90, 90);
order.setLastlegOrderId(99L);
LastlegOrder tail = new LastlegOrder(); tail.setId(99L);
ReturnInventoryCarton first = returnCarton(101L, "RC-A", "P-1", 50);
ReturnInventoryCarton second = returnCarton(102L, "RC-B", "P-2", 40);
when(orders.findNo("customer-1", "RT-MULTI")).thenReturn(order);
when(orderItems.lockOrder(1L)).thenReturn(List.of(item(11L, "SKU-A", 50, 50), item(12L, "SKU-B", 40, 40)));
when(fbaWarehouseValidator.required("200", "XLX6", null)).thenReturn(
new FbaWarehouseValidator.VerifiedFbaWarehouse(200L, "XLX6", "FBA XLX6", "US", "CA"));
when(numbers.next(eq("RELABEL"), eq("10"), anyLong())).thenReturn("RL-MULTI");
when(lastlegOrders.selectById(99L)).thenReturn(tail);
when(cartons.findAvailable("customer-1", "10")).thenReturn(List.of(first, second));
when(cartons.lockId(101L)).thenReturn(first);
when(cartons.lockId(102L)).thenReturn(second);
when(cartonItems.listCarton(101L)).thenReturn(List.of(returnCartonItem("SKU-A", 50)));
when(cartonItems.listCarton(102L)).thenReturn(List.of(returnCartonItem("SKU-B", 40)));
var result = service.createRelabel("customer-1", "merchant-1", "RT-MULTI",
new ReturnOrderRequests.RelabelCreate(List.of(
new ReturnOrderRequests.RelabelItem("SKU-A", firstQuantity),
new ReturnOrderRequests.RelabelItem("SKU-B", secondQuantity)),
"SKU_TOTAL", "200", "XLX6", "multi-key", "multi SKU acceptance"));
assertEquals("RL-MULTI", result.relabelNo());
var application = ArgumentCaptor.forClass(RelabelApplication.class);
verify(relabelApplications).insert(application.capture());
assertEquals(firstQuantity + secondQuantity, application.getValue().getNotifiedPackageCount().longValue());
assertEquals(2, application.getValue().getItemCount());
var lines = ArgumentCaptor.forClass(RelabelItem.class);
verify(relabelItems, times(2)).insert(lines.capture());
assertEquals(List.of("SKU-A", "SKU-B"), lines.getAllValues().stream().map(RelabelItem::getSkuCode).toList());
assertEquals(List.of((int) firstQuantity, (int) secondQuantity),
lines.getAllValues().stream().map(RelabelItem::getNotifiedPackageCount).toList());
assertTrue(lines.getAllValues().stream().allMatch(line -> "RETURN_BULK_SKU_LABEL".equals(line.getOperationType())));
assertEquals("RELABEL_PENDING", first.getStatus());
assertEquals("RELABEL_PENDING", second.getStatus());
}
private static ReturnInventoryCarton returnCarton(long id, String mark, String pallet, long quantity) {
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setId(id); carton.setCustomerId("customer-1"); carton.setOverseasWarehouseId("10");
carton.setSourceReturnNo("RT-MULTI"); carton.setCartonMark(mark); carton.setPalletCode(pallet);
carton.setTotalQuantity(quantity); carton.setStatus("AVAILABLE");
return carton;
}
private static ReturnInventoryCartonItem returnCartonItem(String sku, long quantity) {
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem();
line.setSkuCode(sku); line.setQuantity(quantity); return line;
}
@Test
void startReturnsOnlyRemainingSkuLinesForTheBoundWarehouse() {
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PARTIALLY_RECEIVED, 8, 3);
@@ -110,6 +191,26 @@ class ReturnReceiptServiceTest {
assertEquals("RETU-1", result.order().lastlegOrderNo());
}
@Test
void rejectingForecastMarksReturnAndLinkedTailAsRejected() {
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_REVIEW, 2, 0);
order.setLastlegOrderId(99L);
LastlegOrder tail = new LastlegOrder();
tail.setId(99L);
when(orders.findNo("customer-1", "RT-1")).thenReturn(order);
when(lastlegOrders.selectById(99L)).thenReturn(tail);
service.review("customer-1", "RT-1", new ReturnOrderRequests.Review("REJECT", "资料不齐"));
assertEquals("REJECTED", order.getStatus());
assertEquals("资料不齐", order.getRemark());
assertEquals("REJECTED", tail.getCurrentStatus());
assertEquals("RETURN_RELABEL:REJECTED", tail.getSubOrderStatus());
assertEquals("REJECTED", tail.getRelabelStatus());
verify(orders).updateById(order);
verify(lastlegOrders).updateById(tail);
}
@Test
void skuLookupReturnsOnlyActiveCandidatesFromTheBoundWarehouse() {
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_RECEIPT, 5, 1);
@@ -162,7 +263,7 @@ class ReturnReceiptServiceTest {
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PARTIALLY_RECEIVED, 2, 1);
ReturnOrderItem line = item(11L, "SKU-A", 2, 1);
stubNewReceipt(order, List.of(line), "RC-AUTO");
var request = confirm("SKU", null, null, "key-final",
var request = confirm("SKU", null, "RC-AUTO", "key-final",
List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)));
var first = service.confirm(identity, request);
@@ -175,7 +276,7 @@ class ReturnReceiptServiceTest {
assertTrue(first.fullyMatched());
assertEquals(ReturnReceiptService.COMPLETED, first.orderStatus());
assertEquals(first.receiptNo(), replay.receiptNo());
assertEquals("RC-AUTO", replay.cartonMark());
assertEquals("RC-AUTO", replay.palletCode());
verify(inventory, times(1)).createReceiptCarton(any(), any(), any(), any());
verify(batches, times(1)).insert(any(ReturnReceiptBatch.class));
verify(receipts, times(1)).insert(any(ReturnReceipt.class));
@@ -249,7 +350,7 @@ class ReturnReceiptServiceTest {
var request = confirm("RETURN_NO", null, null, "concurrent-key", List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)));
ReturnOrder completed = order("RT-1", "10", ReturnReceiptService.COMPLETED, 1, 1);
ReturnReceipt committed = new ReturnReceipt();
committed.setReceiptNo("RR-COMMITTED"); committed.setReceiptBatchNo("RB-COMMITTED"); committed.setCartonMark("RC-COMMITTED");
committed.setReceiptNo("RR-COMMITTED"); committed.setReceiptBatchNo("RB-COMMITTED"); committed.setPalletCode("RC-COMMITTED");
committed.setRequestHash(java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
.digest(new ObjectMapper().writeValueAsBytes(request))));
when(orders.lockNo("RT-1")).thenReturn(completed);
@@ -301,8 +402,8 @@ class ReturnReceiptServiceTest {
return item;
}
private static PdaReturnReceiptRequests.Confirm confirm(String startedBy, String trackingNo, String cartonMark,
private static PdaReturnReceiptRequests.Confirm confirm(String startedBy, String trackingNo, String palletCode,
String key, List<PdaReturnReceiptRequests.Item> items) {
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, cartonMark, items, key);
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, palletCode, items, key);
}
}
@@ -46,6 +46,18 @@ class FbaWarehouseValidatorTest {
assertThat(result.region()).isEqualTo("CA");
}
@Test
void acceptsMatchingIdAndCodeWhenCallerDoesNotCarryWarehouseName() {
when(client.activeWarehouse(193000000000000001L, "internal-token"))
.thenReturn(ApiResponse.success(detail("FBA", "ACTIVE", "XLX1", "XLX1")));
var result = validator.required("193000000000000001", "XLX1", null);
assertThat(result.id()).isEqualTo(193000000000000001L);
assertThat(result.code()).isEqualTo("XLX1");
assertThat(result.name()).isEqualTo("XLX1");
}
@Test
void rejectsAnActualOverseasReceivingWarehouseAsFbaDestination() {
when(client.activeWarehouse(193000000000000001L, "internal-token"))
@@ -352,6 +352,42 @@ class WarehouseTransferServiceTest {
assertEquals("WAREHOUSE_PICKUP", transfer.getValue().getDeliveryMethod());
}
@Test
void bindingDestinationReleasesLinkedRelabelForPreDispatch() {
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_FBA_DESTINATION);
transfer.setNeedRelabel(true);
transfer.setRelabelNo("FBR-1");
transfer.setFbaWarehouseCode(null);
LastlegOrder order = tailOrder();
RelabelApplication relabel = new RelabelApplication();
relabel.setId(71L);
relabel.setCustomerId("TENANT-A");
relabel.setRelabelNo("FBR-1");
relabel.setFulfillmentStatus("WAIT_FBA_DESTINATION");
InboundPlan plan = new InboundPlan();
plan.setId(81L);
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
when(orders.selectById(transfer.getLastlegOrderId())).thenReturn(order);
when(orderDetails.selectList(any())).thenReturn(List.of());
when(inboundPlans.findBySourceOrder("TENANT-A", transfer.getLastlegOrderId())).thenReturn(plan);
when(inboundPlanLines.selectList(any())).thenReturn(List.of());
when(inboundPlanPallets.selectList(any())).thenReturn(List.of());
when(relabelApplications.lockNo("TENANT-A", "FBR-1")).thenReturn(relabel);
when(transferItems.selectList(any())).thenReturn(List.of());
when(sourceOrders.selectList(any())).thenReturn(List.of());
when(transferPallets.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of());
service.bindDestination("OP-1", new WarehouseTransferRequests.BindDestination(
List.of(new WarehouseTransferRequests.DestinationSelection("TENANT-A", "WTR-1")),
"FBA", null, "ONT8", "ONT8", null, null, null, null, "BIND-1"));
assertEquals(WarehouseTransferService.READY_FOR_DISPATCH, transfer.getStatus());
assertEquals("WAIT_PRE_DISPATCH", relabel.getFulfillmentStatus());
verify(relabelApplications).updateById(relabel);
}
@Test
void newTransitInboundCreatesSnapshotAndImmutableCartonMarksWithoutHistoricalInventory() {
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
@@ -645,6 +681,7 @@ class WarehouseTransferServiceTest {
ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.StockIn.class);
verify(inventory).stockIn(eq("TENANT-A"), eq("USER-1"), stockIn.capture());
assertEquals(5, stockIn.getValue().packages());
assertEquals("Customer A", stockIn.getValue().customerName());
assertEquals("SKU-TRANSFER", stockIn.getValue().skuCode());
assertEquals("WTR-1", stockIn.getValue().businessOrderNo());
assertEquals("WAREHOUSE_TRANSFER_RECEIPT:WTR-1:501", stockIn.getValue().idempotencyKey());
@@ -840,6 +877,7 @@ class WarehouseTransferServiceTest {
private static InventoryBalance balance(long id, long inboundOrderId, String inboundCode) {
InventoryBalance balance = new InventoryBalance();
balance.setId(id); balance.setCustomerId("TENANT-A"); balance.setInboundOrderId(inboundOrderId);
balance.setCustomerName("Customer A");
balance.setInboundCode(inboundCode); balance.setSkuCode("SKU-TRANSFER");
return balance;
}