From 183b242d453ed1e2450293d63b5b636f2aa8e976 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:01:17 +0800 Subject: [PATCH] test: cover PDA return receipt lifecycle --- .../service/ReturnReceiptServiceTest.java | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java new file mode 100644 index 000000000..37bbda188 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java @@ -0,0 +1,240 @@ +package com.baiyue.lastleg.returnreceipt.service; + +import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity; +import com.baiyue.lastleg.returnreceipt.dto.PdaReturnReceiptRequests; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrderItem; +import com.baiyue.lastleg.returnreceipt.entity.ReturnReceipt; +import com.baiyue.lastleg.returnreceipt.entity.ReturnReceiptBatch; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper; +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.service.LastlegOrderNoGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ReturnReceiptServiceTest { + @Mock ReturnOrderMapper orders; + @Mock ReturnOrderItemMapper orderItems; + @Mock ReturnReceiptMapper receipts; + @Mock ReturnReceiptItemMapper receiptItems; + @Mock ReturnReceiptBatchMapper batches; + @Mock ReturnInventoryService inventory; + @Mock LastlegOrderNoGenerator numbers; + @Mock LastlegOrderMapper lastlegOrders; + + ReturnReceiptService service; + PdaWarehouseIdentity identity; + + @BeforeEach + void setUp() { + service = new ReturnReceiptService(); + ReflectionTestUtils.setField(service, "orders", orders); + ReflectionTestUtils.setField(service, "orderItems", orderItems); + ReflectionTestUtils.setField(service, "receipts", receipts); + ReflectionTestUtils.setField(service, "receiptItems", receiptItems); + ReflectionTestUtils.setField(service, "batches", batches); + ReflectionTestUtils.setField(service, "inventory", inventory); + ReflectionTestUtils.setField(service, "numbers", numbers); + ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); + ReflectionTestUtils.setField(service, "lastlegOrders", lastlegOrders); + identity = new PdaWarehouseIdentity("operator-1", "仓管员", 10L); + } + + @Test + void startReturnsOnlyRemainingSkuLinesForTheBoundWarehouse() { + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PARTIALLY_RECEIVED, 8, 3); + ReturnOrderItem remaining = item(11L, "SKU-A", 5, 2); + ReturnOrderItem completed = item(12L, "SKU-B", 3, 3); + when(orders.findActiveNo("10", "RT-1")).thenReturn(order); + when(orderItems.listOrder(1L)).thenReturn(List.of(remaining, completed)); + + var result = service.start(identity, new PdaReturnReceiptRequests.Start(" RT-1 ")); + + assertEquals("RT-1", result.returnNo()); + assertEquals(ReturnReceiptService.PARTIALLY_RECEIVED, result.status()); + assertEquals(List.of("SKU-A"), result.expectedItems().stream().map(item -> item.skuCode()).toList()); + assertEquals(2, result.expectedItems().get(0).receivedQuantity()); + } + + @Test + void skuLookupReturnsOnlyActiveCandidatesFromTheBoundWarehouse() { + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_RECEIPT, 5, 1); + ReturnOrderItem item = item(11L, "SKU-A", 5, 1); + when(orderItems.activeSkuCandidates("10", "SKU-A")).thenReturn(List.of(item)); + when(orders.selectBatchIds(any())).thenReturn(List.of(order)); + + var result = service.skuCandidates(identity, new PdaReturnReceiptRequests.SkuLookup(" SKU-A ")); + + assertEquals("SKU-A", result.skuCode()); + assertEquals(1, result.candidates().size()); + assertEquals("RT-1", result.candidates().get(0).returnNo()); + assertEquals(1, result.candidates().get(0).skuReceivedQuantity()); + verify(orderItems).activeSkuCandidates("10", "SKU-A"); + } + + @Test + void partialReceiptCreatesOneCartonAndBillingBatchAndKeepsOrderOpen() { + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_RECEIPT, 5, 1); + ReturnOrderItem first = item(11L, "SKU-A", 3, 1); + ReturnOrderItem second = item(12L, "SKU-B", 2, 0); + stubNewReceipt(order, List.of(first, second), "RC-1"); + + var result = service.confirm(identity, confirm("RETURN_NO", "TRACK-1", "RC-1", "key-1", + List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)))); + + assertFalse(result.fullyMatched()); + assertEquals(ReturnReceiptService.PARTIALLY_RECEIVED, result.orderStatus()); + assertEquals(2, result.receivedQuantity()); + assertEquals(2, first.getReceivedQuantity()); + assertEquals(0, second.getReceivedQuantity()); + assertEquals(ReturnReceiptService.PARTIALLY_RECEIVED, order.getStatus()); + verify(inventory).createReceiptCarton(eq(order), any(ReturnReceipt.class), eq("RC-1"), any()); + verify(batches).insert(any(ReturnReceiptBatch.class)); + } + + @Test + void finalReceiptCompletesTheOrderAndIdempotentReplayDoesNotWriteTwice() { + 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", + List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1))); + + var first = service.confirm(identity, request); + ArgumentCaptor savedReceipt = ArgumentCaptor.forClass(ReturnReceipt.class); + verify(receipts).insert(savedReceipt.capture()); + when(receipts.findKey("operator-1", "key-final")).thenReturn(savedReceipt.getValue()); + when(orders.selectById(1L)).thenReturn(order); + var replay = service.confirm(identity, request); + + assertTrue(first.fullyMatched()); + assertEquals(ReturnReceiptService.COMPLETED, first.orderStatus()); + assertEquals(first.receiptNo(), replay.receiptNo()); + assertEquals("RC-AUTO", replay.cartonMark()); + 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)); + } + + @Test + void rejectsOverReceiptBeforeCreatingReceiptInventoryOrBatch() { + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PARTIALLY_RECEIVED, 2, 1); + ReturnOrderItem line = item(11L, "SKU-A", 2, 1); + when(receipts.findKey("operator-1", "key-over")).thenReturn(null); + when(orders.lockNo("RT-1")).thenReturn(order); + when(orderItems.lockOrder(1L)).thenReturn(List.of(line)); + + ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.confirm(identity, + confirm("RETURN_NO", null, null, "key-over", List.of(new PdaReturnReceiptRequests.Item("SKU-A", 2))))); + + assertEquals(409, error.getStatusCode().value()); + verify(receipts, never()).insert(any(ReturnReceipt.class)); + verifyNoInteractions(inventory, batches); + } + + @Test + void rejectsAnotherWarehousesOrderBeforeLockingSkuOrWritingInventory() { + ReturnOrder order = order("RT-1", "11", ReturnReceiptService.PENDING_RECEIPT, 2, 0); + when(receipts.findKey("operator-1", "key-cross")).thenReturn(null); + when(orders.lockNo("RT-1")).thenReturn(order); + + ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.confirm(identity, + confirm("RETURN_NO", null, null, "key-cross", List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1))))); + + assertEquals(404, error.getStatusCode().value()); + verify(orderItems, never()).lockOrder(any()); + verifyNoInteractions(inventory, batches); + } + + @Test + void rejectsReuseOfAnIdempotencyKeyForDifferentReceiptContent() { + ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_RECEIPT, 2, 0); + ReturnOrderItem line = item(11L, "SKU-A", 2, 0); + stubNewReceipt(order, List.of(line), "RC-1"); + var original = confirm("RETURN_NO", "TRACK-1", "RC-1", "key-1", + List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1))); + service.confirm(identity, original); + ArgumentCaptor savedReceipt = ArgumentCaptor.forClass(ReturnReceipt.class); + verify(receipts).insert(savedReceipt.capture()); + when(receipts.findKey("operator-1", "key-1")).thenReturn(savedReceipt.getValue()); + + ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.confirm(identity, + confirm("RETURN_NO", "TRACK-CHANGED", "RC-1", "key-1", + List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1))))); + + assertEquals(409, error.getStatusCode().value()); + verify(inventory, times(1)).createReceiptCarton(any(), any(), anyString(), any()); + verify(batches, times(1)).insert(any(ReturnReceiptBatch.class)); + } + + private void stubNewReceipt(ReturnOrder order, List items, String cartonMark) { + when(receipts.findKey(eq("operator-1"), anyString())).thenReturn(null); + when(orders.lockNo("RT-1")).thenReturn(order); + when(orderItems.lockOrder(1L)).thenReturn(items); + when(orderItems.listOrder(1L)).thenReturn(items); + when(numbers.next(eq("RETURN_RECEIPT"), eq("10"), anyLong())).thenReturn("RR-1"); + when(numbers.next(eq("RETURN_BATCH"), eq("10"), anyLong())).thenReturn("RB-1"); + ReturnInventoryCarton carton = new ReturnInventoryCarton(); + carton.setCartonMark(cartonMark); + when(inventory.createReceiptCarton(eq(order), any(ReturnReceipt.class), any(), any())).thenReturn(carton); + } + + private static ReturnOrder order(String returnNo, String warehouseId, String status, long expected, long received) { + ReturnOrder order = new ReturnOrder(); + order.setId(1L); + order.setCustomerId("customer-1"); + order.setReturnNo(returnNo); + order.setCustomerReferenceNo("REF-1"); + order.setOverseasWarehouseId(warehouseId); + order.setStatus(status); + order.setExpectedQuantity(expected); + order.setReceivedQuantity(received); + return order; + } + + private static ReturnOrderItem item(long id, String sku, long expected, long received) { + ReturnOrderItem item = new ReturnOrderItem(); + item.setId(id); + item.setReturnOrderId(1L); + item.setCustomerId("customer-1"); + item.setSkuCode(sku); + item.setProductNameZh("商品 " + sku); + item.setExpectedQuantity(expected); + item.setReceivedQuantity(received); + return item; + } + + private static PdaReturnReceiptRequests.Confirm confirm(String startedBy, String trackingNo, String cartonMark, + String key, List items) { + return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, cartonMark, items, key); + } +}