test: cover PDA return receipt lifecycle

This commit is contained in:
2026-09-11 15:01:17 +08:00
parent 3b97eedc3d
commit 183b242d45
@@ -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<ReturnReceipt> 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<ReturnReceipt> 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<ReturnOrderItem> 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<PdaReturnReceiptRequests.Item> items) {
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, cartonMark, items, key);
}
}