test: cover PDA return receipt lifecycle
This commit is contained in:
+240
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user