贴标申请,仓储转运

This commit is contained in:
2026-10-08 15:55:47 +08:00
parent e09f8785e1
commit 16d2d9111a
160 changed files with 5704 additions and 4799 deletions
@@ -40,6 +40,11 @@ import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
import com.baiyue.lastleg.dropship.model.DropshipOutboundCandidate;
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.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -58,9 +63,11 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
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.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -113,6 +120,9 @@ class DeliveryTransferServiceTest {
@Mock DropshipCartonSkuMapper dropshipCartonSkus;
@Mock DropshipSkuInventoryMapper dropshipSkuInventory;
@Mock WarehouseTransferPalletMapper warehouseTransferPallets;
@Mock ReturnInventoryCartonMapper returnCartons;
@Mock ReturnInventoryCartonItemMapper returnCartonItems;
@Mock ReturnInventoryBalanceMapper returnBalances;
private DeliveryTransferService service;
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
@@ -152,6 +162,228 @@ class DeliveryTransferServiceTest {
ReflectionTestUtils.setField(service, "dropshipCartonSkus", dropshipCartonSkus);
ReflectionTestUtils.setField(service, "dropshipSkuInventory", dropshipSkuInventory);
ReflectionTestUtils.setField(service, "warehouseTransferPallets", warehouseTransferPallets);
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
ReflectionTestUtils.setField(service, "returnCartonItems", returnCartonItems);
ReflectionTestUtils.setField(service, "returnBalances", returnBalances);
}
@Test
void returnBoxOutboundCanSelectOneCartonFromAPallet() {
ReturnInventoryCarton first = returnCarton("RC-1", "PAL-1");
ReturnInventoryCarton second = returnCarton("RC-2", "PAL-1");
when(returnCartons.lockMark("RC-1")).thenReturn(first);
when(returnCartons.listActivePallet("WH-1", "PAL-1")).thenReturn(List.of(first, second));
service.createReturnOutbound("TENANT-A", "U-1",
new DeliveryRequests.ReturnOutbound("receiver", "WH-1", "RT-1", List.of("RC-1"), null, "return-1"));
ArgumentCaptor<OutboundPalletItem> item = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundPallets).insert(item.capture());
assertNull(item.getValue().getPalletNo());
verify(outboundOrders).insert(any(OutboundOrder.class));
}
@Test
void receivedPalletIsEligibleForWholePalletOutboundButParcelIsNot() {
ReturnInventoryCarton pallet = returnCarton("RC-PAL", "PAL-1");
pallet.setSourceUnitType("PALLET");
ReturnInventoryCarton parcel = returnCarton("RC-PARCEL", null);
parcel.setSourceUnitType("PARCEL");
when(returnCartons.findAvailable("TENANT-A", "WH-1")).thenReturn(List.of(pallet, parcel));
var candidates = service.returnOutboundCandidates("TENANT-A", "WH-1");
assertEquals(List.of("RC-PAL"), candidates.stream().map(DeliveryResponses.ReturnOutboundCandidate::cartonMark).toList());
}
@Test
void singleSkuReturnOutboundReservesParcelAndCreatesDelivery() {
ReturnInventoryCarton parcel = returnCarton("RC-PARCEL", null);
parcel.setId(901L); parcel.setSourceUnitType("PARCEL"); parcel.setTotalQuantity(3L);
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem();
line.setSkuCode("SKU-A"); line.setQuantity(3L);
when(returnCartons.lockMark("RC-PARCEL")).thenReturn(parcel);
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(line));
service.createReturnOutbound("TENANT-A", "U-1",
new DeliveryRequests.ReturnOutbound("receiver", "WH-1", "RT-1",
List.of("RC-PARCEL"), null, "sku-return-1", "SKU-A"));
ArgumentCaptor<OutboundPalletItem> item = ArgumentCaptor.forClass(OutboundPalletItem.class);
ArgumentCaptor<DeliveryOrder> delivery = ArgumentCaptor.forClass(DeliveryOrder.class);
verify(outboundPallets).insert(item.capture());
verify(deliveryOrders).insert(delivery.capture());
assertEquals("SKU-A", item.getValue().getSkuCode());
assertEquals("RC-PARCEL", item.getValue().getCartonMarks());
assertEquals("OUTBOUND_PENDING", parcel.getStatus());
assertEquals("WAIT_DELIVERY", delivery.getValue().getStatus());
assertEquals(1, delivery.getValue().getPackageCount());
}
@Test
void singleSkuReturnOutboundRejectsMixedCarton() {
ReturnInventoryCarton mixed = returnCarton("RC-MIXED", null);
mixed.setId(901L); mixed.setTotalQuantity(5L);
ReturnInventoryCartonItem skuA = new ReturnInventoryCartonItem(); skuA.setSkuCode("SKU-A"); skuA.setQuantity(3L);
ReturnInventoryCartonItem skuB = new ReturnInventoryCartonItem(); skuB.setSkuCode("SKU-B"); skuB.setQuantity(2L);
when(returnCartons.lockMark("RC-MIXED")).thenReturn(mixed);
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(skuA, skuB));
assertEquals(409, assertThrows(ResponseStatusException.class, () -> service.createReturnOutbound("TENANT-A", "U-1",
new DeliveryRequests.ReturnOutbound("receiver", "WH-1", "RT-1",
List.of("RC-MIXED"), null, "sku-return-2", "SKU-A"))).getStatusCode().value());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
}
@Test
void singleSkuReturnPickAcceptsSkuScanAndRejectsOtherSku() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "sku-return-1");
outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = new OutboundPalletItem(); item.setId(801L); item.setCustomerId("TENANT-A");
item.setOutboundOrderId(301L); item.setSkuCode("SKU-A"); item.setCartonMarks("RC-1");
item.setPackageCount(1); item.setPickedPackageCount(0);
ReturnInventoryCarton carton = returnCarton("RC-1", null); carton.setId(901L);
carton.setStatus("OUTBOUND_PENDING"); carton.setTotalQuantity(3L);
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(3L);
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
when(outboundPallets.selectById(801L)).thenReturn(item);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(returnCartons.lockMark("RC-1")).thenReturn(carton);
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(line));
when(returnBalances.shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L)).thenReturn(1);
assertEquals(409, assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(801L, null, 1L, "wrong-sku", List.of(), "SKU-B"))).getStatusCode().value());
service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(801L, null, 1L, "correct-sku", List.of(), "SKU-A"));
assertEquals("OUTBOUNDED", carton.getStatus());
verify(returnBalances).shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L);
}
@Test
void returnDeliveryDetailShowsFrozenCartonSkuAndQuantity() {
DeliveryOrder delivery = new DeliveryOrder(); delivery.setId(501L); delivery.setCustomerId("TENANT-A");
delivery.setDeliveryNo("DL-501"); delivery.setOutboundOrderId(301L);
delivery.setOutboundNo("OUT-301"); delivery.setOverseasWarehouseId("WH-1");
delivery.setDeliveryGroupKey("RETURN:RT-1"); delivery.setStatus("WAIT_DELIVERY");
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "return-1");
outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("RETURN_OUTBOUND");
OutboundPalletItem item = new OutboundPalletItem(); item.setCartonMarks("RC-1");
ReturnInventoryCarton carton = returnCarton("RC-1", null); carton.setId(901L);
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem();
line.setSkuCode("SKU-A"); line.setProductNameZh("测试品"); line.setQuantity(3L);
when(deliveryOrders.findByNo("TENANT-A", "DL-501")).thenReturn(delivery);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(returnCartons.findCustomerMark("TENANT-A", "RC-1")).thenReturn(carton);
when(returnCartonItems.listCarton(901L)).thenReturn(List.of(line));
var detail = service.deliveryDetail("TENANT-A", "DL-501");
assertEquals(1, detail.skus().size());
assertEquals("SKU-A", detail.skus().get(0).skuCode());
assertEquals(3L, detail.skus().get(0).productQuantity());
assertEquals(1, detail.skus().get(0).packageCount());
}
@Test
void returnCartonPickDeductsEverySkuOnce() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "return-1");
outbound.setCustomerId("TENANT-A"); outbound.setOverseasWarehouseId("WH-1");
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = new OutboundPalletItem(); item.setId(801L); item.setCustomerId("TENANT-A");
item.setOutboundOrderId(301L); item.setPackageCount(1); item.setPickedPackageCount(0);
item.setCartonMarks("RC-1"); item.setStatus("WAIT_PICK");
ReturnInventoryCarton carton = returnCarton("RC-1", null); carton.setStatus("OUTBOUND_PENDING");
carton.setId(901L);
ReturnInventoryCartonItem skuA = new ReturnInventoryCartonItem(); skuA.setSkuCode("SKU-A"); skuA.setQuantity(3L);
ReturnInventoryCartonItem skuB = new ReturnInventoryCartonItem(); skuB.setSkuCode("SKU-B"); skuB.setQuantity(2L);
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
when(outboundPallets.selectById(801L)).thenReturn(item);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(returnCartons.lockMark("RC-1")).thenReturn(carton);
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(skuA, skuB));
when(returnBalances.shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L)).thenReturn(1);
when(returnBalances.shipCartonSku("TENANT-A", "WH-1", "SKU-B", 2L)).thenReturn(1);
service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(801L, null, 1L, "scan-rc-1", List.of("RC-1")));
verify(returnBalances).shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L);
verify(returnBalances).shipCartonSku("TENANT-A", "WH-1", "SKU-B", 2L);
assertEquals("OUTBOUNDED", carton.getStatus());
assertEquals("PICKED", item.getStatus());
}
@Test
void returnPalletRejectsIndividualCartonScan() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "return-2");
outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("RETURN_OUTBOUND");
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = new OutboundPalletItem(); item.setId(801L); item.setCustomerId("TENANT-A");
item.setOutboundOrderId(301L); item.setPalletNo("PAL-1"); item.setCartonMarks("RC-1");
item.setPackageCount(1); item.setPickedPackageCount(0);
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
when(outboundPallets.selectById(801L)).thenReturn(item);
assertEquals(409, assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(801L, null, 1L, "scan-rc-1", List.of("RC-1")))).getStatusCode().value());
verify(returnBalances, never()).shipCartonSku(any(), any(), any(), anyLong());
}
@Test
void returnPalletScanShipsEveryCartonAndRejectsChangedPalletContents() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "return-3");
outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("RETURN_OUTBOUND");
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = new OutboundPalletItem(); item.setId(801L); item.setCustomerId("TENANT-A");
item.setOutboundOrderId(301L); item.setPalletNo("PAL-1"); item.setCartonMarks("RC-1,RC-2");
item.setPackageCount(2); item.setPickedPackageCount(0);
ReturnInventoryCarton first = returnCarton("RC-1", "PAL-1"); first.setId(901L); first.setStatus("OUTBOUND_PENDING");
ReturnInventoryCarton second = returnCarton("RC-2", "PAL-1"); second.setId(902L); second.setStatus("OUTBOUND_PENDING");
ReturnInventoryCartonItem firstSku = new ReturnInventoryCartonItem(); firstSku.setSkuCode("SKU-A"); firstSku.setQuantity(3L);
ReturnInventoryCartonItem secondSku = new ReturnInventoryCartonItem(); secondSku.setSkuCode("SKU-B"); secondSku.setQuantity(2L);
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(returnCartons.listActivePallet("WH-1", "PAL-1")).thenReturn(List.of(first));
assertEquals(409, assertThrows(ResponseStatusException.class, () -> service.pdaPickPallet("WH-1", "U-1", "PDA-701",
"PAL-1", new DeliveryRequests.PdaPalletPick("scan-pal-1"))).getStatusCode().value());
verify(returnBalances, never()).shipCartonSku(any(), any(), any(), anyLong());
when(returnCartons.listActivePallet("WH-1", "PAL-1")).thenReturn(List.of(first, second));
when(returnCartons.lockMark("RC-1")).thenReturn(first);
when(returnCartons.lockMark("RC-2")).thenReturn(second);
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(firstSku));
when(returnCartonItems.lockCarton(902L)).thenReturn(List.of(secondSku));
when(returnBalances.shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L)).thenReturn(1);
when(returnBalances.shipCartonSku("TENANT-A", "WH-1", "SKU-B", 2L)).thenReturn(1);
service.pdaPickPallet("WH-1", "U-1", "PDA-701", "PAL-1", new DeliveryRequests.PdaPalletPick("scan-pal-2"));
assertEquals("OUTBOUNDED", first.getStatus()); assertEquals("OUTBOUNDED", second.getStatus());
assertEquals(2, item.getPickedPackageCount());
verify(returnBalances).shipCartonSku("TENANT-A", "WH-1", "SKU-A", 3L);
verify(returnBalances).shipCartonSku("TENANT-A", "WH-1", "SKU-B", 2L);
}
private static ReturnInventoryCarton returnCarton(String mark, String palletCode) {
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setCustomerId("TENANT-A"); carton.setOverseasWarehouseId("WH-1");
carton.setSourceReturnNo("RT-1"); carton.setSourceUnitType("BOX");
carton.setCartonMark(mark); carton.setPalletCode(palletCode); carton.setStatus("AVAILABLE");
return carton;
}
@Test
@@ -0,0 +1,146 @@
package com.baiyue.lastleg.delivery.service;
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
import com.baiyue.lastleg.delivery.entity.*;
import com.baiyue.lastleg.delivery.mapper.*;
import com.baiyue.lastleg.returnreceipt.entity.*;
import com.baiyue.lastleg.returnreceipt.mapper.*;
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
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.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import static org.assertj.core.api.Assertions.*;
class ReturnOutboundClosureTest {
final DeliveryTransferService service = new DeliveryTransferService();
final OutboundOrderMapper outbounds = mock(OutboundOrderMapper.class);
final OutboundPalletItemMapper items = mock(OutboundPalletItemMapper.class);
final OutboundPdaTaskMapper tasks = mock(OutboundPdaTaskMapper.class);
final DeliveryOrderMapper deliveries = mock(DeliveryOrderMapper.class);
final ReturnInventoryCartonMapper cartons = mock(ReturnInventoryCartonMapper.class);
final ReturnInventoryCartonItemMapper contents = mock(ReturnInventoryCartonItemMapper.class);
final ReturnInventoryService stock = mock(ReturnInventoryService.class);
final OutboundPdaPickMapper picks = mock(OutboundPdaPickMapper.class);
final com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper relabelApplications = mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class);
final com.baiyue.lastleg.relabel.service.RelabelApplicationService relabelService = mock(com.baiyue.lastleg.relabel.service.RelabelApplicationService.class);
ReturnInventoryCarton carton;
@BeforeEach void setup() {
ReflectionTestUtils.setField(service, "outboundOrders", outbounds);
ReflectionTestUtils.setField(service, "outboundPallets", items);
ReflectionTestUtils.setField(service, "pdaTasks", tasks);
ReflectionTestUtils.setField(service, "deliveryOrders", deliveries);
ReflectionTestUtils.setField(service, "returnCartons", cartons);
ReflectionTestUtils.setField(service, "returnCartonItems", contents);
ReflectionTestUtils.setField(service, "returnStock", stock);
ReflectionTestUtils.setField(service, "pdaPicks", picks);
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
ReflectionTestUtils.setField(service, "relabelService", relabelService);
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
carton = new ReturnInventoryCarton(); carton.setId(10L); carton.setCartonMark("RC#1-1");
carton.setCustomerId("C1"); carton.setOverseasWarehouseId("WH1"); carton.setSourceReturnNo("RT1,RT2");
carton.setStatus("AVAILABLE"); carton.setSourceUnitType("BOX"); carton.setTotalQuantity(6L); carton.setSkuTypes(1);
when(cartons.lockMark(carton.getCartonMark())).thenReturn(carton);
when(stock.matchesReturnSource(carton, "RT1")).thenReturn(true);
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem(); line.setSkuCode("SKU1"); line.setQuantity(6L);
when(contents.lockCarton(10L)).thenReturn(List.of(line));
}
DeliveryRequests.ReturnOutbound command(String sku) {
return new DeliveryRequests.ReturnOutbound("address", "WH1", "RT1", List.of("RC#1-1"), null, "KEY1", sku);
}
@Test void reservesWholeMergedBoxAndCreatesDelivery() {
service.createReturnOutbound("C1", "USER", command(null));
assertThat(carton.getStatus()).isEqualTo("OUTBOUND_PENDING");
verify(items).insert(argThat((OutboundPalletItem i) -> i.getPackageCount() == 1 && "RT1,RT2".equals(i.getInboundCode())));
verify(deliveries).insert(argThat((DeliveryOrder d) -> "WAIT_DELIVERY".equals(d.getStatus()) && d.getPackageCount() == 1));
}
@Test void refusesSecondCommandForReservedBox() {
service.createReturnOutbound("C1", "USER", command(null));
assertThatThrownBy(() -> service.createReturnOutbound("C1", "USER", command(null)))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("占用");
verify(outbounds, times(1)).insert(any(OutboundOrder.class));
}
@Test void preventsSingleSkuBypassingExistingBox() {
assertThatThrownBy(() -> service.createReturnOutbound("C1", "USER", command("SKU1")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("按箱唛");
verify(outbounds, never()).insert(any(OutboundOrder.class));
}
@Test void singleSkuParcelReservesAndKeepsScanSku() {
carton.setSourceUnitType("PARCEL");
service.createReturnOutbound("C1", "USER", command("SKU1"));
verify(items).insert(argThat((OutboundPalletItem i) -> "SKU1".equals(i.getSkuCode())));
assertThat(carton.getStatus()).isEqualTo("OUTBOUND_PENDING");
}
@Test void refusesPackingReservedQuantity() {
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem(); line.setSkuCode("SKU1");
line.setQuantity(6L); line.setPackingReservedQuantity(1L);
when(contents.lockCarton(10L)).thenReturn(List.of(line));
assertThatThrownBy(() -> service.createReturnOutbound("C1", "USER", command(null)))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("合箱计划");
verify(outbounds, never()).insert(any(OutboundOrder.class));
}
@Test void exactScanIsRequiredOnReplayAsWell() {
OutboundPdaTask task = new OutboundPdaTask(); task.setId(20L); task.setCustomerId("C1"); task.setOutboundOrderId(30L);
when(tasks.lockByNoAndWarehouse("TASK1", "WH1")).thenReturn(task);
OutboundPdaPick pick = new OutboundPdaPick(); pick.setTaskId(20L); pick.setOutboundPalletItemId(40L); pick.setPackages(1L);
when(picks.findByKey("C1", "PICK1")).thenReturn(pick);
OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("C1"); outbound.setOutboundType("RETURN_OUTBOUND");
when(outbounds.selectById(30L)).thenReturn(outbound);
OutboundPalletItem item = new OutboundPalletItem(); item.setId(40L); item.setCustomerId("C1"); item.setOutboundOrderId(30L);
item.setPackageCount(1); item.setCartonMarks("RC#1-1"); when(items.selectById(40L)).thenReturn(item);
assertThatThrownBy(() -> service.pdaPick("WH1", "USER", "TASK1",
new DeliveryRequests.PdaPick(40L, null, 1L, "PICK1", List.of("WRONG"))))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("指定的箱唛");
verify(stock, never()).shipCarton(anyString(), anyString(), anyString(), anyString(), anyString(), anyLong());
}
@Test void cancelsUntouchedNativeRelabelWithItsOwningApplication() {
OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("C1");
outbound.setOutboundNo("OUT1"); outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK");
when(outbounds.findGlobalByNo("OUT1")).thenReturn(outbound);
when(outbounds.lockGlobalByNo("OUT1")).thenReturn(outbound);
var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication();
application.setId(50L); application.setCustomerId("C1"); application.setRelabelNo("REL1");
application.setOutboundOrderNo("OUT1"); application.setStatus("READY_FOR_OUTBOUND");
when(relabelApplications.lockByOutbound("C1", "OUT1")).thenReturn(application);
OutboundPalletItem item = new OutboundPalletItem(); item.setCartonMarks("RC#1-1"); item.setStatus("WAIT_PICK");
item.setPickedPackageCount(0); when(items.findByOutbound("C1", 30L)).thenReturn(List.of(item));
carton.setStatus("OUTBOUND_PENDING"); carton.setRelabelApplicationId(50L);
service.cancelOutbound("OUT1", "OP1", new DeliveryRequests.Cancel("cancel"));
assertThat(carton.getStatus()).isEqualTo("RELABEL_PENDING");
assertThat(outbound.getStatus()).isEqualTo("CANCELLED");
verify(relabelService).changeStatus(eq("C1"), eq("REL1"), eq("OP1"),
argThat(request -> "CANCELLED".equals(request.status())));
var sequence = inOrder(relabelApplications, outbounds, cartons, relabelService);
sequence.verify(relabelApplications).lockByOutbound("C1", "OUT1");
sequence.verify(outbounds).lockGlobalByNo("OUT1");
sequence.verify(cartons).lockMark("RC#1-1");
sequence.verify(relabelService).changeStatus(anyString(), anyString(), anyString(), any());
}
@Test void cancellationRejectsPickedNativeBoxWithoutReleasingItsReservation() {
OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("C1");
outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK");
when(outbounds.lockGlobalByNo("OUT1")).thenReturn(outbound);
OutboundPalletItem item = new OutboundPalletItem(); item.setStatus("WAIT_PICK"); item.setPickedPackageCount(1);
when(items.findByOutbound("C1", 30L)).thenReturn(List.of(item));
assertThatThrownBy(() -> service.cancelOutbound("OUT1", "OP1", new DeliveryRequests.Cancel("cancel")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("execution has started");
verifyNoInteractions(relabelService);
verify(cartons, never()).updateById(any(ReturnInventoryCarton.class));
}
}
@@ -108,6 +108,7 @@ class CargoDiscardServiceTest {
assertEquals(2, detail.summary().totalPackages());
assertEquals(new BigDecimal("10.00"), detail.summary().totalFee());
assertEquals(2, storedLinks.size());
verify(orders, never()).insert(any(LastlegOrder.class));
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
}
@@ -59,6 +59,7 @@ class DispatchPdaWorkflowServiceTest {
@Mock InventoryPositionMapper positions;
@Mock InventoryCommandService inventory;
@Mock DispatchPickOrderAttributionService attribution;
@Mock com.baiyue.lastleg.dispatch.service.ReturnRelabelDispatchStock returnStock;
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
private DispatchPdaWorkflowService service;
private ObjectMapper objectMapper;
@@ -80,6 +81,7 @@ class DispatchPdaWorkflowServiceTest {
ReflectionTestUtils.setField(service, "positions", positions);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "pickAttribution", attribution);
ReflectionTestUtils.setField(service, "returnRelabelStock", returnStock);
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
}
@@ -329,6 +331,31 @@ class DispatchPdaWorkflowServiceTest {
return value;
}
@Test void nativeBoxPickLocksOrderBeforeStockAndNeverUsesOrdinaryInventory() {
var order = order("100", "DSP-1", "WH-1", "PENDING_DEPARTURE");
order.setVehiclePlate("CA-100");
var task = task(11L, "100", "WH-1", "PDA_PENDING");
var item = item(21L, "100", 2);
item.setSourceStage("RETURN_RELABEL"); item.setSourceBusinessType("RELABEL_ORDER"); item.setOrderId(101L);
when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order);
when(orders.lockById("100", 1L)).thenReturn(order);
when(tasks.lockByDispatch("100", 1L)).thenReturn(task);
when(items.selectById(21L)).thenReturn(item);
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); carton.setId(71L);
when(returnStock.pick(org.mockito.ArgumentMatchers.eq(item), org.mockito.ArgumentMatchers.eq("RC1#2-1"),
org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq(3L), anyLong(), org.mockito.ArgumentMatchers.eq("DSP-1"))).thenReturn(carton);
var result = service.pick("WH-1", "DSP-1", 21L, "7", "作业员",
new DispatchPdaRequests.Pick(null, 1, 3, "native-pick", null, "RC1#2-1"));
assertEquals(1L, result.pickedPackages());
var sequence = org.mockito.Mockito.inOrder(attribution, returnStock, picks);
sequence.verify(attribution).prepareNativeReturn("100", 101L);
sequence.verify(returnStock).pick(org.mockito.ArgumentMatchers.eq(item), org.mockito.ArgumentMatchers.eq("RC1#2-1"),
org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq(3L), anyLong(), org.mockito.ArgumentMatchers.eq("DSP-1"));
sequence.verify(picks).insert(org.mockito.ArgumentMatchers.argThat((DispatchPdaPick p) -> p.getReturnCartonId().equals(71L) && p.getInventoryPositionId() == null));
sequence.verify(attribution).attributeNativeReturn(org.mockito.ArgumentMatchers.eq("100"), any(), org.mockito.ArgumentMatchers.eq(101L));
org.mockito.Mockito.verifyNoInteractions(inventory, positions);
}
private static DispatchOrder order(String customerId, String dispatchNo, String warehouseId, String status) {
DispatchOrder value = new DispatchOrder();
value.setId(1L); value.setCustomerId(customerId); value.setDispatchNo(dispatchNo);
@@ -44,6 +44,14 @@ class DispatchPickOrderAttributionServiceTest {
< sql.indexOf("shipped_package_count=shipped_package_count+"));
}
@Test void nativePreparationRejectsHeldOrderBeforeAnyAttribution() {
var held = new LastlegOrder(); held.setDetained(true);
when(orders.lockById("t1", 101L)).thenReturn(held);
assertThrows(org.springframework.web.server.ResponseStatusException.class,
() -> service().prepareNativeReturn("t1", 101L));
verifyNoInteractions(attributions);
}
@Test
void attributesPackagesAcrossInboundOrdersInReceiptFifoOrder() {
DispatchPickOrderAttributionService service = service();
@@ -64,6 +64,7 @@ class DispatchServiceTest {
@Mock OutboundOrderMapper outboundOrders;
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
@Mock ReturnRelabelDispatchStock returnRelabelStock;
private DispatchService service;
@BeforeEach
@@ -71,6 +72,7 @@ class DispatchServiceTest {
service = new DispatchService();
ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow);
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
ReflectionTestUtils.setField(service, "returnRelabelStock", returnRelabelStock);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "bolLines", bolLines);
@@ -437,24 +439,6 @@ class DispatchServiceTest {
verify(bolLines).findByDispatchOrder("200", 1L);
}
@Test
void filtersStageBeforePaginationAndPreservesTheServerTotal() {
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(
new org.apache.ibatis.builder.MapperBuilderAssistant(new com.baomidou.mybatisplus.core.MybatisConfiguration(), "test"), DispatchOrder.class);
Page<DispatchOrder> resultPage = new Page<>(2, 10, 47);
resultPage.setRecords(List.of());
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenAnswer(invocation -> {
LambdaQueryWrapper<DispatchOrder> wrapper = invocation.getArgument(1);
assertTrue(wrapper.getSqlSegment().contains("status IN"));
assertTrue(wrapper.getParamNameValuePairs().values().containsAll(
List.of("PICKING", "READY_TO_DEPART", "PARTIAL_READY_TO_DEPART")));
return resultPage;
});
var result = service.platformPage(new DispatchRequests.Query(2, 10, null, null, null, null, DispatchRequests.Stage.LOADING));
assertEquals(47, result.total());
assertEquals(List.of("DEPARTED", "PARTIAL_DEPARTED"), DispatchRequests.Stage.AFTER_LOADING.statuses());
}
@Test
void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() {
DispatchOrder first = new DispatchOrder();
@@ -0,0 +1,78 @@
package com.baiyue.lastleg.dispatch.service;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ReturnRelabelDispatchStockTest {
@Mock DispatchCandidateMapper candidates;
@Mock ReturnInventoryCartonMapper cartons;
@Mock PalletPhysicalGateway physical;
@Mock ReturnInventoryService inventory;
ReturnRelabelDispatchStock service;
DispatchItem item;
ReturnInventoryCarton carton;
@BeforeEach void setup() {
service = new ReturnRelabelDispatchStock();
ReflectionTestUtils.setField(service, "candidates", candidates);
ReflectionTestUtils.setField(service, "cartons", cartons);
ReflectionTestUtils.setField(service, "physical", physical);
ReflectionTestUtils.setField(service, "inventory", inventory);
item = new DispatchItem(); item.setCustomerId("C"); item.setOverseasWarehouseId("9");
item.setPalletNo("P1"); item.setCartonMarks("RC1#2-1,RC1#2-2"); item.setSourceBusinessNo("RL1");
carton = new ReturnInventoryCarton(); carton.setId(11L); carton.setCustomerId("C"); carton.setOverseasWarehouseId("9");
carton.setCartonMark("RC1#2-1"); carton.setPalletCode("P1"); carton.setRelabelNo("RL1");
carton.setSourceReturnNo("RT1,RT2"); carton.setTotalQuantity(6L); carton.setStatus("OUTBOUND_PENDING");
}
@Test void candidatesResolveRealCarrierWithoutCreatingOrdinaryStock() {
var row = new DispatchPalletCandidate().palletNo("P1").overseasWarehouseId("9").returnCartonSource(true);
when(candidates.nativeReturnRelabelAvailable("C", null, "RL1")).thenReturn(List.of(row));
when(physical.resolve("P1")).thenReturn(new PalletPhysicalGateway.PalletReference(55L, "P1", 9L, 4L, "OCCUPIED", "ONT8", 0));
assertEquals(55L, service.candidates("C", null, "RL1").get(0).warehousePalletId());
verifyNoInteractions(inventory, cartons);
}
@Test void wrongScanAndMultiBoxRequestNeverDeductStock() {
assertThrows(ResponseStatusException.class, () -> service.pick(item, "OTHER", 1, 6, 100, "DIS1"));
assertThrows(ResponseStatusException.class, () -> service.pick(item, "RC1#2-1", 2, 6, 100, "DIS1"));
verifyNoInteractions(inventory, cartons);
}
@Test void foreignCustomerOrChangedCarrierNeverDeductsStock() {
when(cartons.lockMark("RC1#2-1")).thenReturn(carton);
carton.setCustomerId("OTHER");
assertThrows(ResponseStatusException.class, () -> service.pick(item, "RC1#2-1", 1, 6, 100, "DIS1"));
carton.setCustomerId("C"); carton.setPalletCode("P2");
assertThrows(ResponseStatusException.class, () -> service.pick(item, "RC1#2-1", 1, 6, 100, "DIS1"));
verifyNoInteractions(inventory);
}
@Test void exactBoxShipsItsFullSourceSetAndRejectsStaleQuantity() {
when(cartons.lockMark("RC1#2-1")).thenReturn(carton);
assertThrows(ResponseStatusException.class, () -> service.pick(item, "RC1#2-1", 1, 5, 100, "DIS1"));
verifyNoInteractions(inventory);
assertEquals(11L, service.pick(item, "RC1#2-1", 1, 6, 100, "DIS1").getId());
verify(inventory).shipCarton("C", "9", "RT1,RT2", "RC1#2-1", "DIS1", 100);
}
@Test void cancelledDispatchRestoresRelabelReservationWithoutMakingItPackable() {
item.setCartonMarks("RC1#2-1");
when(cartons.lockMark("RC1#2-1")).thenReturn(carton);
when(cartons.updateById(carton)).thenReturn(1);
service.release(item);
assertEquals("RELABEL_PENDING", carton.getStatus()); assertEquals("RL1", carton.getRelabelNo());
verifyNoInteractions(inventory);
}
}
@@ -136,8 +136,8 @@ class RelabelContractTest {
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','DISCARD_PENDING')ANDSKU_TYPES>0ANDTOTAL_QUANTITY>0"));
assertTrue(baseline.contains("STATUSIN('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','OUTBOUNDED','DISCARD_PENDING')ANDSKU_TYPES>0ANDTOTAL_QUANTITY>0"));
assertTrue(baseline.contains("STATUSIN('MERGED','DISCARDED')ANDSKU_TYPES=0ANDTOTAL_QUANTITY=0"));
assertTrue(baseline.contains("STATUSIN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED')"));
assertTrue(baseline.contains("STATUSIN('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED')"));
}
}
@@ -0,0 +1,53 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.mapper.*;
import com.baiyue.lastleg.relabel.entity.*;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class ReturnRelabelDeliveryTest {
@Test void directNativeRelabelCreatesTwoPhysicalBoxesAndOneDeliveryPerDestination() {
var service = new RelabelApplicationService();
var items = mock(RelabelItemMapper.class);
var cartons = mock(ReturnInventoryCartonMapper.class);
var deliveries = mock(DeliveryOrderMapper.class);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "returnCartons", cartons);
ReflectionTestUtils.setField(service, "deliveryOrders", deliveries);
ReflectionTestUtils.setField(service, "outboundOrders", mock(OutboundOrderMapper.class));
ReflectionTestUtils.setField(service, "outboundItems", mock(OutboundPalletItemMapper.class));
ReflectionTestUtils.setField(service, "outboundTasks", mock(OutboundPdaTaskMapper.class));
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
var application = new RelabelApplication(); application.setId(1L); application.setCustomerId("C1");
application.setRelabelNo("REL1"); application.setOverseasWarehouseId("WH1"); application.setDeliveryMethod("EXPRESS");
var a = source(10L, "A"); var b = source(11L, "B");
when(items.selectList(any())).thenReturn(List.of(a, b));
for (var item : List.of(a, b)) {
var carton = new ReturnInventoryCarton(); carton.setId(item.getReturnCartonId());
carton.setCustomerId("C1"); carton.setRelabelApplicationId(1L); carton.setStatus("RELABEL_PENDING");
when(cartons.lockId(carton.getId())).thenReturn(carton);
}
ReflectionTestUtils.invokeMethod(service, "createDirectOutbound", application, "OP1", LocalDateTime.now());
verify(deliveries, times(2)).insert(any(DeliveryOrder.class));
verify(deliveries).insert(argThat((DeliveryOrder d) -> "A".equals(d.getDestinationWarehouseCode())
&& d.getPackageCount() == 1 && "WAIT_DELIVERY".equals(d.getStatus())
&& application.getOutboundOrderNo().equals(d.getOutboundNo())));
verify(deliveries).insert(argThat((DeliveryOrder d) -> "B".equals(d.getDestinationWarehouseCode()) && d.getPackageCount() == 1));
}
private RelabelItem source(long id, String destination) {
var item = new RelabelItem(); item.setId(id); item.setReturnCartonId(id);
item.setNotifiedPackageCount(1); item.setSourceCartonMark("RC" + id + "#1-1");
item.setDestinationFbaWarehouseCode(destination); return item;
}
}
@@ -50,6 +50,9 @@ class ReturnInventoryServiceTest {
@Mock LastlegOrderNoGenerator numbers;
@Mock ReturnOrderMapper orders;
@Mock ReturnReceiptMapper receipts;
@Mock com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper packingTrace;
@Mock com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingPlanMapper packingPlans;
@Mock com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper orderItems;
@Mock com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway physicalPallets;
private ReturnInventoryService service;
@@ -66,6 +69,62 @@ class ReturnInventoryServiceTest {
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "receiptMapper", receipts);
ReflectionTestUtils.setField(service, "physicalPallets", physicalPallets);
ReflectionTestUtils.setField(service, "packingTrace", packingTrace);
ReflectionTestUtils.setField(service, "packingPlans", packingPlans);
ReflectionTestUtils.setField(service, "orderItems", orderItems);
ReflectionTestUtils.setField(service, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules());
}
@Test
void platformPlanNumberFilterIsTrimmedAndUsedForCountAndPage() {
when(packingPlans.countPlatform(null, "PLAN-1", "C1", "W1", "SKU1")).thenReturn(1L);
when(packingPlans.listPlatform(null, "PLAN-1", "C1", "W1", "SKU1", 20L, 20L)).thenReturn(List.of());
var result = service.platformPackingPlans(new ReturnInventoryRequests.PackingPlanQuery(2, 20, null, " PLAN-1 ", " C1 ", " W1 ", " SKU1 "));
assertThat(result.total()).isEqualTo(1L);
verify(packingPlans).listPlatform(null, "PLAN-1", "C1", "W1", "SKU1", 20L, 20L);
}
@Test
void cartonDetailsExposeImmutableMergeHistoryWithinCustomerScope() {
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setId(1L); carton.setCustomerId("CUSTOMER-A"); carton.setCartonMark("TARGET-1"); carton.setStatus("AVAILABLE");
when(cartons.findCustomerMark("CUSTOMER-A", "TARGET-1")).thenReturn(carton);
when(cartonItems.listCarton(1L)).thenReturn(List.of());
var history = new com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.MergeHistory("PLAN-1", "SOURCE-1", "TARGET-1", "RETURN-1", "SKU-1", 2, java.time.LocalDateTime.now());
when(packingTrace.listCartonHistory("CUSTOMER-A", "TARGET-1")).thenReturn(List.of(history));
assertThat(service.carton("CUSTOMER-A", "TARGET-1").mergeHistory()).containsExactly(history);
assertThrows(ResponseStatusException.class, () -> service.carton("CUSTOMER-B", "TARGET-1"));
}
@Test
void packingSourcesBatchItemsWithinCustomerWarehouseAndSubtractReservations() {
ReturnInventoryCarton first = new ReturnInventoryCarton();
first.setId(1L); first.setCartonMark("PARCEL-1"); first.setSourceUnitType("PARCEL"); first.setSourceReturnNo("RETURN-1");
ReturnInventoryCarton second = new ReturnInventoryCarton();
second.setId(2L); second.setCartonMark("BOX-2"); second.setSourceUnitType("BOX");
ReturnInventoryCartonItem item = new ReturnInventoryCartonItem();
item.setCartonId(1L); item.setSkuCode("SKU-1"); item.setQuantity(9L); item.setPackingReservedQuantity(7L);
when(cartons.platformCount("customer", "warehouse", null, null, "AVAILABLE", null)).thenReturn(2L);
when(cartons.platformPage("customer", "warehouse", null, null, "AVAILABLE", null, 0L, 200L)).thenReturn(List.of(first, second));
when(cartonItems.listCartons(List.of(1L, 2L))).thenReturn(List.of(item));
var response = service.platformPackingSourcePage(new ReturnInventoryRequests.PlatformQuery(1, 200, "customer", "warehouse", null, null, null, null));
assertThat(response.total()).isEqualTo(2);
assertThat(response.records().get(0).sourceUnitType()).isEqualTo("PARCEL");
assertThat(response.records().get(0).items().get(0).availableQuantity()).isEqualTo(2);
assertThat(response.records().get(1).items()).isEmpty();
verify(cartonItems).listCartons(List.of(1L, 2L));
verify(cartonItems, never()).listCarton(anyLong());
verifyNoInteractions(balances);
}
@Test
void emptyPackingSourcesSkipItemQueriesAndRequireCustomerScope() {
when(cartons.platformCount("customer", "warehouse", null, null, "AVAILABLE", null)).thenReturn(0L);
var response = service.platformPackingSourcePage(new ReturnInventoryRequests.PlatformQuery(1, 200, "customer", "warehouse", null, null, null, null));
assertThat(response.records()).isEmpty();
verifyNoInteractions(cartonItems);
assertThrows(ResponseStatusException.class, () -> service.platformPackingSourcePage(
new ReturnInventoryRequests.PlatformQuery(1, 200, null, "warehouse", null, null, null, null)));
}
@Test
@@ -82,7 +141,8 @@ class ReturnInventoryServiceTest {
when(cartons.findCustomerMark(carton.getCustomerId(), carton.getCartonMark())).thenReturn(carton);
when(cartonItems.listCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
String html = service.label(carton.getCustomerId(), carton.getCartonMark()).printableHtml();
assertThat(html).contains("<svg", "RT-1", "RB-1,RB-2", "—", "box-sizing:border-box");
assertThat(html).contains("<svg", carton.getCartonMark(), "SKU-A", "box-sizing:border-box");
assertThat(html).doesNotContain("RT-1", "RB-1,RB-2", "—");
assertThat(html).doesNotContain("repeating-linear-gradient", "null", carton.getCustomerId(), carton.getOverseasWarehouseId());
}
@@ -110,6 +170,8 @@ class ReturnInventoryServiceTest {
receipt.setId(21L);
receipt.setReceiptBatchNo("RB-1");
receipt.setPalletCode("PAL-RETURN-1");
receipt.setUnitType("PALLET");
receipt.setUnitCode("PAL-RETURN-1");
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-1");
service.createReceiptCarton(order, receipt, null,
@@ -120,6 +182,30 @@ class ReturnInventoryServiceTest {
assertThat(captor.getValue().getSourceReturnNo()).isEqualTo("RT-1");
}
@Test
void palletReceiptCreatesOnePrintableCartonPerSkuBox() {
ReturnOrder order = completedOrder();
ReturnReceipt receipt = new ReturnReceipt();
receipt.setId(21L);
receipt.setReceiptBatchNo("RB-1");
receipt.setPalletCode("PAL-RETURN-1");
receipt.setUnitType("PALLET");
receipt.setUnitCode("PAL-RETURN-1");
List<ReturnInventoryCarton> created = service.createReceiptCartons(order, receipt,
List.of(new ReturnInventoryService.ReceiptLine("SKU-A", "商品A", 5)), java.util.Map.of("SKU-A", 2L));
assertThat(created).hasSize(2);
assertThat(created).extracting(ReturnInventoryCarton::getCartonMark)
.allMatch(mark -> mark.matches("RC[A-F0-9]{16}#2-[12]"))
.doesNotHaveDuplicates();
assertThat(created.get(0).getCartonMark()).endsWith("#2-1");
assertThat(created.get(1).getCartonMark()).endsWith("#2-2");
assertThat(created).extracting(ReturnInventoryCarton::getTotalQuantity).containsExactly(3L, 2L);
assertThat(created).extracting(ReturnInventoryCarton::getPalletCode).containsOnly("PAL-RETURN-1");
verify(cartons, org.mockito.Mockito.times(2)).insert(any(ReturnInventoryCarton.class));
}
@Test
void mergeMovesItemsBetweenCartonsWithoutReducingAvailableQuantity() {
ReturnOrder order = completedOrder();
@@ -133,6 +219,7 @@ class ReturnInventoryServiceTest {
source.setSourceBatchNo("RB-1");
source.setSourceReturnNo("RT-1");
source.setStatus(ReturnInventoryService.AVAILABLE);
source.setSourceUnitType("PARCEL");
source.setSkuTypes(2);
source.setTotalQuantity(4L);
ReturnInventoryCartonItem skuA = item(41L, "SKU-A", 2L);
@@ -140,12 +227,10 @@ class ReturnInventoryServiceTest {
when(orders.findByNos("42", List.of("RT-1"))).thenReturn(List.of(order));
when(receipts.listOrder(11L)).thenReturn(List.of(receipt));
when(cartons.findReceipt(21L)).thenReturn(source);
when(cartons.listReceipt(21L)).thenReturn(List.of(source));
when(cartons.lockMark("RC-SOURCE")).thenReturn(source);
when(cartonItems.lockCarton(31L)).thenReturn(List.of(skuA, skuB));
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-MERGED");
when(physicalPallets.ensureRelabelPallet(org.mockito.ArgumentMatchers.eq(42L), anyString()))
.thenReturn(new com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway.PalletReference(91L, "PLT-MERGED", 42L, null, "EMPTY", null, 0));
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
service.merge(new PdaWarehouseIdentity("operator-1", "操作员", 42L),
@@ -162,10 +247,10 @@ class ReturnInventoryServiceTest {
verify(cartons).insert(target.capture());
assertThat(target.getValue().getSourceBatchNo()).isEqualTo("RB-1");
assertThat(target.getValue().getSourceReturnNo()).isEqualTo("RT-1");
assertThat(target.getValue().getPalletCode()).isEqualTo("PLT-MERGED");
ArgumentCaptor<String> businessKey = ArgumentCaptor.forClass(String.class);
verify(physicalPallets).ensureRelabelPallet(org.mockito.ArgumentMatchers.eq(42L), businessKey.capture());
assertThat(businessKey.getValue()).matches("RETURN-MERGE:[0-9a-f]{64}");
assertThat(target.getValue().getPalletCode()).isNull();
verifyNoInteractions(physicalPallets);
verify(packingTrace, org.mockito.Mockito.times(2)).insert(anyLong(), anyString(), anyLong(), anyString(), anyString(), anyString(), anyString(), anyLong(), any());
}
@Test
@@ -198,8 +283,6 @@ class ReturnInventoryServiceTest {
ReturnInventoryCarton locked = prepareConcurrentSource();
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
when(physicalPallets.ensureRelabelPallet(org.mockito.ArgumentMatchers.eq(42L), anyString()))
.thenReturn(new com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway.PalletReference(92L, "PLT-TARGET", 42L, null, "EMPTY", null, 0));
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
service.merge(new PdaWarehouseIdentity("operator-2", "操作员2", 42L), mergeRequest(2));
assertThat(locked.getTotalQuantity()).isZero();
@@ -264,16 +347,97 @@ class ReturnInventoryServiceTest {
return operation;
}
@Test
void skuPackingNeverConsumesPhysicalBoxInventory() {
ReturnInventoryCarton source = prepareConcurrentSource();
source.setSourceUnitType("BOX");
// The candidate snapshot is a parcel; the locked physical source must be rechecked.
var error = assertThrows(ResponseStatusException.class, () -> service.merge(
new PdaWarehouseIdentity("operator-2", "操作员", 42L), mergeRequest(1)));
assertThat(error.getStatusCode().value()).isEqualTo(409);
verify(cartons, never()).insert(any(ReturnInventoryCarton.class));
}
@Test
void samePrefixSkusHaveIndependentShortBoxMarksAndTheirOwnSequences() {
String a = ReflectionTestUtils.invokeMethod(service, "shortReturnMark", "RT-ORDER", 123L, "SAMEPREFIX-A", 2L, 1L);
String b = ReflectionTestUtils.invokeMethod(service, "shortReturnMark", "RT-ORDER", 123L, "SAMEPREFIX-B", 2L, 1L);
String second = ReflectionTestUtils.invokeMethod(service, "shortReturnMark", "RT-ORDER", 123L, "SAMEPREFIX-A", 2L, 2L);
assertThat(a).isNotEqualTo(b).matches("RC[A-F0-9]{16}#2-1");
assertThat(second).isEqualTo(a.substring(0, a.lastIndexOf('-') + 1) + "2");
}
@Test
void packingLabelsCountBoxesPerSkuInsteadOfCountingAllSkusTogether() {
var boxA = new ReturnInventoryRequests.PlanBox(List.of(new ReturnInventoryRequests.PlanItem("A", 2)), null, null, null, null);
var boxB = new ReturnInventoryRequests.PlanBox(List.of(new ReturnInventoryRequests.PlanItem("B", 2)), null, null, null, null);
List<String> marks = ReflectionTestUtils.invokeMethod(service, "packingMarks", List.of(boxA, boxB, boxA));
assertThat(marks).hasSize(3).doesNotHaveDuplicates();
assertThat(marks.get(0)).endsWith("#2-1"); assertThat(marks.get(1)).endsWith("#1-1");
assertThat(marks.get(2)).isEqualTo(marks.get(0).replace("#2-1", "#2-2"));
}
@Test
void reservedSkuQuantityCannotBeConsumedByDirectPacking() {
prepareConcurrentSource();
var line = item(41L, "SKU-A", 2); line.setPackingReservedQuantity(2L);
when(cartonItems.lockCarton(31L)).thenReturn(List.of(line));
var error = assertThrows(ResponseStatusException.class, () -> service.merge(
new PdaWarehouseIdentity("operator-2", "操作员", 42L), mergeRequest(1)));
assertThat(error.getStatusCode().value()).isEqualTo(409);
verify(cartonItems, never()).deleteById(anyLong());
verify(cartonItems, never()).updateById(any(ReturnInventoryCartonItem.class));
verify(cartons, never()).insert(any(ReturnInventoryCarton.class));
}
@Test
void planCreationLocksExactSkuAndCancelReleasesOnlyItsReservation() {
var source = new ReturnInventoryCarton(); source.setId(31L); source.setCartonMark("RC-SOURCE");
source.setCustomerId("10001"); source.setOverseasWarehouseId("42"); source.setSourceReturnNo("RT-1");
source.setStatus("AVAILABLE"); source.setSourceUnitType("PARCEL");
var line = item(41L, "SKU-A", 5); line.setPackingReservedQuantity(1L);
when(orders.lockNo("RT-1")).thenReturn(completedOrder());
when(cartons.packingAvailable("10001", "42")).thenReturn(List.of(source));
when(cartons.lockMark("RC-SOURCE")).thenReturn(source);
when(cartonItems.lockCarton(31L)).thenReturn(List.of(line));
when(cartonItems.lockSku(31L, "SKU-A")).thenReturn(line);
when(balances.reserveAvailable("10001", "42", "SKU-A", 2)).thenReturn(1);
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("PLAN-1");
var request = new ReturnInventoryRequests.PackingPlanCreate(List.of("RT-1"), "SKU",
List.of(new ReturnInventoryRequests.PlanBox(List.of(new ReturnInventoryRequests.PlanItem("SKU-A", 2)), null, null, null, null)), null, "plan-key");
var created = service.createPackingPlan("platform", request);
assertThat(created.status()).isEqualTo("PENDING_REVIEW"); assertThat(line.getPackingReservedQuantity()).isEqualTo(3);
var captor = ArgumentCaptor.forClass(com.baiyue.lastleg.returnreceipt.entity.ReturnPackingPlan.class);
verify(packingPlans).insert(captor.capture());
when(packingPlans.lockNo("PLAN-1")).thenReturn(captor.getValue());
assertThat(service.cancelPackingPlan("PLAN-1").status()).isEqualTo("CANCELLED");
assertThat(line.getPackingReservedQuantity()).isEqualTo(1);
verify(balances).adjust(anyLong(), org.mockito.ArgumentMatchers.eq("10001"), org.mockito.ArgumentMatchers.eq("42"), org.mockito.ArgumentMatchers.eq("SKU-A"), org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.eq(0L), org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.eq(0L));
}
@Test
void boxPlanRequiresExplicitSourceMarksInsteadOfOrderScopeOnly() {
when(orders.lockNo("RT-1")).thenReturn(completedOrder());
var request = new ReturnInventoryRequests.PackingPlanCreate(List.of("RT-1"), "BOX",
List.of(new ReturnInventoryRequests.PlanBox(List.of(), null, null, null, null)), null, "box-key");
var error = assertThrows(ResponseStatusException.class, () -> service.createPackingPlan("platform", request));
assertThat(error.getStatusCode().value()).isEqualTo(400);
verifyNoInteractions(cartonItems, balances);
}
private ReturnInventoryCarton prepareConcurrentSource() {
ReturnReceipt receipt = new ReturnReceipt(); receipt.setId(21L);
ReturnInventoryCarton snapshot = new ReturnInventoryCarton();
snapshot.setId(31L); snapshot.setCartonMark("RC-SOURCE"); snapshot.setStatus(ReturnInventoryService.AVAILABLE);
snapshot.setSourceUnitType("PARCEL");
snapshot.setSourceReturnNo("RT-1");
ReturnInventoryCarton locked = new ReturnInventoryCarton();
locked.setId(31L); locked.setCartonMark("RC-SOURCE"); locked.setStatus(ReturnInventoryService.AVAILABLE);
locked.setSourceUnitType("PARCEL");
locked.setCustomerId("10001"); locked.setOverseasWarehouseId("42"); locked.setSourceBatchNo("RB-1"); locked.setSourceReturnNo("RT-1");
when(orders.findByNos("42", List.of("RT-1"))).thenReturn(List.of(completedOrder()));
when(receipts.listOrder(11L)).thenReturn(List.of(receipt));
when(cartons.findReceipt(21L)).thenReturn(snapshot);
when(cartons.listReceipt(21L)).thenReturn(List.of(snapshot));
when(cartons.lockMark("RC-SOURCE")).thenReturn(locked);
return locked;
}
@@ -39,6 +39,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -498,8 +499,25 @@ class ReturnReceiptServiceTest {
return item;
}
@Test
void combinedLabelsKeepPhysicalPageStyleAndOneSectionPerBox() {
String first = "<html><head><style>@page{size:100mm 100mm}.barcode{height:15mm}</style></head><body><div class='label'>RC#2-1</div></body></html>";
String second = first.replace("RC#2-1", "RC#2-2");
String result = ReflectionTestUtils.invokeMethod(service, "combineLabels", List.of(
new com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses.CartonLabel("RC#2-1", first),
new com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses.CartonLabel("RC#2-2", second)));
assertThat(result).contains("@page{size:100mm 100mm}", ".barcode{height:15mm}", "RC#2-1", "RC#2-2", ".return-label:last-child");
assertThat(result.split("<section class='return-label'>", -1)).hasSize(3);
assertThat(result.split("@page", -1)).hasSize(2);
}
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, palletCode, items, key);
List<PdaReturnReceiptRequests.Item> boxed = items.stream()
.map(item -> new PdaReturnReceiptRequests.Item(item.skuCode(), item.receivedQuantity(), 1L)).toList();
// These focused tests exercise receipt/idempotency behavior at the
// carton level. Pallet receipts have a separate multi-carton contract.
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, palletCode, boxed, key,
"BOX", palletCode, null, null, null, null, 1L);
}
}