贴标申请,仓储转运
This commit is contained in:
+1
-1
@@ -46,7 +46,7 @@ class ReturnOutboundPdaPersistenceTest {
|
||||
// Persist production entities through production MyBatis mappers, with real transactions.
|
||||
for (Class<?> entity : List.of(OutboundOrder.class, OutboundPalletItem.class, OutboundPdaTask.class,
|
||||
OutboundPdaPick.class, DeliveryOrder.class, ReturnInventoryCarton.class, ReturnInventoryCartonItem.class, ReturnInventoryBalance.class,
|
||||
ReturnInventoryOutboundMovement.class, ReturnOrder.class, ReturnOrderItem.class, ReturnReceipt.class, ReturnReceiptItem.class)) {
|
||||
ReturnInventoryOutboundMovement.class, ReturnSkuConversion.class, ReturnOrder.class, ReturnOrderItem.class, ReturnReceipt.class, ReturnReceiptItem.class)) {
|
||||
String columns = Arrays.stream(entity.getDeclaredFields()).filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> field.getName().replaceAll("([A-Z])", "_$1").toLowerCase() + " "
|
||||
+ (field.getType() == String.class ? "VARCHAR(2000)" : field.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.baiyue.lastleg.relabel;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RelabelMilestonesPersistenceTest {
|
||||
JdbcTemplate jdbc;
|
||||
RelabelApplicationMapper mapper;
|
||||
final LocalDateTime review = LocalDateTime.of(2026, 10, 12, 10, 0);
|
||||
final LocalDateTime completion = review.plusHours(1);
|
||||
final LocalDateTime outbound = review.plusHours(2);
|
||||
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL", "sa", "");
|
||||
// Keep the in-memory database alive across mapper connections.
|
||||
ds.setUrl(ds.getUrl() + ";DB_CLOSE_DELAY=-1");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE ll_relabel_application(id BIGINT, customer_id VARCHAR(128), status VARCHAR(32), relabel_no VARCHAR(64), outbound_order_no VARCHAR(64))");
|
||||
jdbc.execute("CREATE TABLE ll_relabel_status_history(customer_id VARCHAR(128), application_id BIGINT, from_status VARCHAR(32), to_status VARCHAR(32), created_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_relabel_pda_task(customer_id VARCHAR(128), application_id BIGINT, submitted_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_outbound_order(id BIGINT, customer_id VARCHAR(128), outbound_no VARCHAR(64), status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE ll_outbound_pda_task(customer_id VARCHAR(128), outbound_order_id BIGINT, completed_at TIMESTAMP, status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE ll_warehouse_transfer_order(customer_id VARCHAR(128), relabel_application_id BIGINT, relabel_no VARCHAR(64), outbound_at TIMESTAMP)");
|
||||
var config = new MybatisConfiguration();
|
||||
config.setMapUnderscoreToCamelCase(true);
|
||||
config.addMapper(RelabelApplicationMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(ds); factory.setConfiguration(config);
|
||||
mapper = new SqlSessionTemplate(factory.getObject()).getMapper(RelabelApplicationMapper.class);
|
||||
jdbc.update("INSERT INTO ll_relabel_application VALUES (1,'C','PENDING_REVIEW','RL-1',NULL)");
|
||||
}
|
||||
|
||||
void history(String from, String to, LocalDateTime at) {
|
||||
jdbc.update("INSERT INTO ll_relabel_status_history VALUES ('C',1,?,?,?)", from, to, at);
|
||||
}
|
||||
|
||||
@Test void historicalCompletedApplicationUsesActualFactsAndCustomerScope() {
|
||||
history("PENDING_REVIEW", "PENDING_PROCESSING", review);
|
||||
history("PROCESSING", "CUSTOMER_CONFIRM_PENDING", completion.plusMinutes(1));
|
||||
history("READY_FOR_OUTBOUND", "COMPLETED", outbound.plusMinutes(1));
|
||||
jdbc.update("INSERT INTO ll_relabel_pda_task VALUES ('C',1,?)", completion);
|
||||
jdbc.update("UPDATE ll_relabel_application SET status='COMPLETED', outbound_order_no='OB-1'");
|
||||
jdbc.update("INSERT INTO ll_outbound_order VALUES (2,'C','OB-1','OUTBOUNDED')");
|
||||
jdbc.update("INSERT INTO ll_outbound_pda_task VALUES ('C',2,?,'COMPLETED')", outbound);
|
||||
jdbc.update("INSERT INTO ll_relabel_status_history VALUES ('OTHER',1,'PENDING_REVIEW','PENDING_PROCESSING',?)", outbound);
|
||||
var result = mapper.findMilestones("C", 1L);
|
||||
assertEquals(review, result.reviewedAt());
|
||||
assertEquals(completion, result.relabelCompletedAt());
|
||||
assertEquals(outbound, result.outboundAt());
|
||||
assertNull(mapper.findMilestones("OTHER", 1L));
|
||||
}
|
||||
|
||||
@Test void pendingAndReworkDoNotInventCompletion() {
|
||||
var pending = mapper.findMilestones("C", 1L);
|
||||
// MyBatis returns null for a projection whose columns are all null; the service handles it.
|
||||
assertNull(pending);
|
||||
history("PENDING_REVIEW", "PENDING_PROCESSING", review);
|
||||
history("PROCESSING", "CUSTOMER_CONFIRM_PENDING", completion);
|
||||
jdbc.update("INSERT INTO ll_relabel_pda_task VALUES ('C',1,?)", completion);
|
||||
jdbc.update("UPDATE ll_relabel_application SET status='PROCESSING'");
|
||||
var rework = mapper.findMilestones("C", 1L);
|
||||
assertEquals(review, rework.reviewedAt());
|
||||
assertNull(rework.relabelCompletedAt()); assertNull(rework.outboundAt());
|
||||
}
|
||||
|
||||
@Test void skuOnlyCompletionIsNotOutboundAndResubmissionUsesLatestReview() {
|
||||
history("PENDING_REVIEW", "REJECTED", review.minusHours(1));
|
||||
history("PENDING_REVIEW", "PENDING_PROCESSING", review);
|
||||
history("PROCESSING", "CUSTOMER_CONFIRM_PENDING", completion);
|
||||
history("CUSTOMER_CONFIRM_PENDING", "COMPLETED", completion.plusMinutes(5));
|
||||
jdbc.update("UPDATE ll_relabel_application SET status='COMPLETED'");
|
||||
var result = mapper.findMilestones("C", 1L);
|
||||
assertEquals(review, result.reviewedAt()); assertEquals(completion, result.relabelCompletedAt());
|
||||
assertNull(result.outboundAt());
|
||||
}
|
||||
|
||||
@Test void truckDepartureAndLinkedTransferUseOutboundFacts() {
|
||||
history("READY_FOR_OUTBOUND", "COMPLETED", outbound);
|
||||
jdbc.update("UPDATE ll_relabel_application SET status='COMPLETED'");
|
||||
assertEquals(outbound, mapper.findMilestones("C", 1L).outboundAt());
|
||||
jdbc.update("DELETE FROM ll_relabel_status_history");
|
||||
jdbc.update("UPDATE ll_relabel_application SET status='READY_FOR_OUTBOUND'");
|
||||
jdbc.update("INSERT INTO ll_warehouse_transfer_order VALUES ('C',1,'RL-1',?)", outbound);
|
||||
assertEquals(outbound, mapper.findMilestones("C", 1L).outboundAt());
|
||||
}
|
||||
}
|
||||
+4
@@ -52,6 +52,7 @@ class PdaRelabelServiceTest {
|
||||
@Mock PdaRelabelHistoryMapper histories;
|
||||
@Mock RelabelApplicationService applicationService;
|
||||
@Mock RelabelPhotoFileGateway photoFiles;
|
||||
@Mock com.baiyue.lastleg.relabel.service.ReturnSkuConversionService skuConversions;
|
||||
PdaRelabelService service;
|
||||
RelabelApplication application;
|
||||
RelabelItem item;
|
||||
@@ -60,6 +61,7 @@ class PdaRelabelServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new PdaRelabelService();
|
||||
ReflectionTestUtils.setField(service, "skuConversions", skuConversions);
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "bindingGuard", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.RelabelBindingGuard.class));
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
@@ -450,6 +452,7 @@ class PdaRelabelServiceTest {
|
||||
new PdaRelabelRequests.Complete(List.of(501L), 1, "complete-1", "done"));
|
||||
|
||||
verify(photoFiles).validate("100", 20L, 501L);
|
||||
verify(skuConversions).apply(eq(application), org.mockito.ArgumentMatchers.anyList(), org.mockito.ArgumentMatchers.anyList(), anyString(), org.mockito.ArgumentMatchers.anyList());
|
||||
assertEquals(PdaRelabelService.WAIT_REVIEW, result.taskStatus());
|
||||
assertEquals("CUSTOMER_CONFIRM_PENDING", result.applicationStatus());
|
||||
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("7"), any());
|
||||
@@ -483,6 +486,7 @@ class PdaRelabelServiceTest {
|
||||
assertEquals("picker", task.getOperatorName());
|
||||
assertTrue(result.completionPhotoFileIds().isEmpty());
|
||||
verify(applicationService).resetForRework("100", "FBR-1", "8", "wrong label");
|
||||
verify(skuConversions).reverse(application);
|
||||
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
|
||||
+58
-7
@@ -66,6 +66,21 @@ import static org.mockito.Mockito.times;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RelabelApplicationServiceTest {
|
||||
@Test void summaryReturnsMilestonesFromCustomerScopedFacts() {
|
||||
var application = new RelabelApplication();
|
||||
application.setId(42L); application.setCustomerId("10001");
|
||||
var reviewed = LocalDateTime.of(2026, 10, 12, 10, 0);
|
||||
var completed = reviewed.plusHours(1);
|
||||
var outbound = reviewed.plusHours(2);
|
||||
when(applications.findMilestones("10001", 42L))
|
||||
.thenReturn(new RelabelResponses.Milestones(reviewed, completed, outbound));
|
||||
RelabelResponses.Summary result = ReflectionTestUtils.invokeMethod(service, "summary", application, List.of());
|
||||
assertNotNull(result);
|
||||
assertEquals(reviewed, result.reviewedAt());
|
||||
assertEquals(completed, result.relabelCompletedAt());
|
||||
assertEquals(outbound, result.outboundAt());
|
||||
}
|
||||
|
||||
@Test void returnInventoryCandidateUsesRealProductNames() {
|
||||
var cartonMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
@@ -85,6 +100,37 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals(4L, candidate.stockQuantity());
|
||||
assertEquals("AVAILABLE", candidate.inventoryStatus());
|
||||
assertEquals(List.of(), candidate.mergedIntoCartonMarks());
|
||||
carton.setStatus("RELABEL_PENDING");
|
||||
var occupied = service.candidates("C", "W", null).get(0);
|
||||
assertEquals(0, occupied.availablePackages());
|
||||
assertEquals(2, occupied.skuItems().size(), "处理中库存保留占用信息,但不可再次选择");
|
||||
}
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(longs = {0, 2, 5, 8})
|
||||
void returnCandidatesOnlyOfferUnlabelledPieces(long completed) {
|
||||
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "returnCartonItems", lines);
|
||||
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setId(20L); carton.setStatus("AVAILABLE"); carton.setTotalQuantity(10L);
|
||||
var first = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
first.setSkuCode("A"); first.setQuantity(5L);
|
||||
var second = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
second.setSkuCode("B"); second.setQuantity(5L); second.setPackingReservedQuantity(1L);
|
||||
when(cartons.findRelabelCandidates("C", "W")).thenReturn(List.of(carton));
|
||||
when(lines.listCarton(20L)).thenReturn(List.of(first, second));
|
||||
when(cartons.completedSkuQuantity(20L, "A")).thenReturn(5L);
|
||||
when(cartons.completedSkuQuantity(20L, "B")).thenReturn(completed);
|
||||
var candidates = service.candidates("C", "W", null);
|
||||
if (completed >= 4) assertTrue(candidates.isEmpty());
|
||||
else {
|
||||
assertEquals(1, candidates.size());
|
||||
assertEquals(4 - completed, candidates.get(0).availablePackages());
|
||||
assertEquals(List.of(new RelabelResponses.SkuItem("B", null, (int) (4 - completed), null)), candidates.get(0).skuItems());
|
||||
assertEquals(10L, candidates.get(0).stockQuantity());
|
||||
}
|
||||
}
|
||||
@Mock RelabelApplicationMapper applications;
|
||||
@Mock RelabelItemMapper items;
|
||||
@@ -110,6 +156,8 @@ class RelabelApplicationServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new RelabelApplicationService();
|
||||
ReflectionTestUtils.setField(service, "skuConversions", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.ReturnSkuConversionService.class));
|
||||
ReflectionTestUtils.setField(service, "skuConversionRows", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class));
|
||||
ReflectionTestUtils.setField(service, "pdaTasks", pdaTasks);
|
||||
lenient().when(pdaTasks.lock(any(), any())).thenAnswer(call -> {
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();
|
||||
@@ -1009,8 +1057,8 @@ class RelabelApplicationServiceTest {
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"CARTON_LABEL", "SKU_LABEL", "CARTON_AND_SKU"})
|
||||
void intactParcelSupportsAllLabelOperationsWithoutMergingIntoBox(String type) {
|
||||
@org.junit.jupiter.params.provider.CsvSource({"CARTON_LABEL,0", "SKU_LABEL,0", "SKU_LABEL,2", "CARTON_AND_SKU,0"})
|
||||
void intactParcelSupportsAllLabelOperationsWithoutMergingIntoBox(String type, long completed) {
|
||||
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons); ReflectionTestUtils.setField(service, "returnCartonItems", lines);
|
||||
@@ -1020,6 +1068,7 @@ class RelabelApplicationServiceTest {
|
||||
parcel.setCountStatus("CONFIRMED"); parcel.setTotalQuantity(5L);
|
||||
var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(5L);
|
||||
when(cartons.lockId(901L)).thenReturn(parcel); when(lines.lockCarton(901L)).thenReturn(List.of(line));
|
||||
when(cartons.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed);
|
||||
when(cartons.markRelabelPending(anyLong(), anyLong(), anyString(), any())).thenReturn(1);
|
||||
var app = application(11L, "RL-PARCEL", "10001", "parcel-key", null, "PENDING_REVIEW"); app.setLastlegOrderId(21L);
|
||||
when(applications.findNo(eq("10001"), any())).thenReturn(app);
|
||||
@@ -1027,7 +1076,7 @@ class RelabelApplicationServiceTest {
|
||||
var detail = new RelabelRequests.Item(null, null, null, "SKU-A", null, null, null,
|
||||
"Return goods", 1, 1, "RT-1", null, "NEW-A", null, null, null, null, null,
|
||||
"PARCEL-1", "FBA-PARCEL", "XLX1", type,
|
||||
"CARTON_LABEL".equals(type) ? List.of() : List.of(new RelabelRequests.SkuItem("SKU-A", "NEW-A", 5, null)), 901L);
|
||||
"CARTON_LABEL".equals(type) ? List.of() : List.of(new RelabelRequests.SkuItem("SKU-A", "NEW-A", (int) (5 - completed), null)), 901L);
|
||||
service.create("10001", "operator", new RelabelRequests.Create("RT-1", "WH-1", LocalDateTime.now().plusDays(1),
|
||||
"Contact", "19900000000", null, null, "parcel-key", List.of(detail), "SKU_LABEL".equals(type) ? null : "EXPRESS", false));
|
||||
var stored = ArgumentCaptor.forClass(RelabelItem.class); verify(items).insert(stored.capture());
|
||||
@@ -1054,8 +1103,9 @@ class RelabelApplicationServiceTest {
|
||||
assertTrue(error.getReason().contains("独立新箱")); verify(applications, never()).insert(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnSkuRelabelRejectsQuantityBeyondSelectedSourceBox() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(longs = {3, 5})
|
||||
void returnSkuRelabelRejectsAlreadyLabelledPieces(long completed) {
|
||||
var cartonMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
var lineMapper = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartonMapper);
|
||||
@@ -1068,14 +1118,15 @@ class RelabelApplicationServiceTest {
|
||||
line.setSkuCode("SKU-A"); line.setQuantity(5L); line.setPackingReservedQuantity(0L);
|
||||
when(cartonMapper.lockId(901L)).thenReturn(carton);
|
||||
when(lineMapper.lockCarton(901L)).thenReturn(List.of(line));
|
||||
when(cartonMapper.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed);
|
||||
var detail = new RelabelRequests.Item(null, null, null, "SKU-A", null, null, null,
|
||||
"Return goods", 1, 1, "RT-1", null, "NEW-A", null, null,
|
||||
null, null, null, "RETURN-BOX-1", null, null, "SKU_LABEL",
|
||||
List.of(new RelabelRequests.SkuItem("SKU-A", "NEW-A", 6, null)), 901L);
|
||||
List.of(new RelabelRequests.SkuItem("SKU-A", "NEW-A", 3, null)), 901L);
|
||||
var request = new RelabelRequests.Create("RT-1", "WH-1", LocalDateTime.now().plusDays(1),
|
||||
"Li", "13800000000", null, null, "return-over-quantity", List.of(detail), null, false);
|
||||
var error = assertThrows(ResponseStatusException.class, () -> service.create("10001", "operator-1", request));
|
||||
assertTrue(error.getReason().contains("超过来源箱可用数量"));
|
||||
assertTrue(error.getReason().contains(completed == 5 ? "已全部换标" : "超过来源箱可用数量"));
|
||||
verify(applications, never()).insert(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ class RelabelReturnCartonCandidateTest {
|
||||
@Test
|
||||
void candidatesIncludeAvailableMergedReturnCartonByWarehouseMark() {
|
||||
RelabelApplicationService service = new RelabelApplicationService();
|
||||
ReflectionTestUtils.setField(service, "skuConversionRows", mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class));
|
||||
InventoryBalanceMapper balances = mock(InventoryBalanceMapper.class);
|
||||
InventoryPositionMapper positions = mock(InventoryPositionMapper.class);
|
||||
ReturnInventoryCartonMapper cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
|
||||
+31
-2
@@ -42,6 +42,34 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReturnInventoryServiceTest {
|
||||
@Test
|
||||
void platformStockIncludesRelabelLockedQuantity() {
|
||||
var balance = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance();
|
||||
balance.setCustomerId("10001"); balance.setOverseasWarehouseId("42"); balance.setSkuCode("SKU-A");
|
||||
balance.setTotalQuantity(100L); balance.setAvailableQuantity(50L); balance.setRelabelLockedQuantity(30L);
|
||||
when(balances.platformCount("10001", "42", "SKU-A")).thenReturn(1L);
|
||||
when(balances.platformPage("10001", "42", "SKU-A", 0L, 20L)).thenReturn(List.of(balance));
|
||||
|
||||
var result = service.platformStockPage(new ReturnInventoryRequests.PlatformQuery(1, 20, "10001", "42", "SKU-A", null, null, null));
|
||||
|
||||
assertThat(result.records().get(0).relabelLockedQuantity()).isEqualTo(30L);
|
||||
assertThat(result.records().get(0).availableQuantity()).isEqualTo(50L);
|
||||
}
|
||||
@Test
|
||||
void summarySeparatesRelabelLockedStockFromOtherUnavailableStock() {
|
||||
var balance = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance();
|
||||
balance.setOverseasWarehouseId("42"); balance.setSkuCode("SKU-A");
|
||||
balance.setTotalQuantity(100L); balance.setAvailableQuantity(50L);
|
||||
balance.setRelabelLockedQuantity(30L); balance.setCartonQuantity(3L);
|
||||
when(balances.countPage("10001", "42", "SKU-A")).thenReturn(1L);
|
||||
when(balances.page("10001", "42", "SKU-A", 0L, 20L)).thenReturn(List.of(balance));
|
||||
|
||||
var result = service.summaryPage("10001", new ReturnInventoryRequests.SummaryQuery(1, 20, "42", "SKU-A"));
|
||||
|
||||
assertThat(result.records().get(0).relabelLockedQuantity()).isEqualTo(30L);
|
||||
assertThat(result.records().get(0).availableQuantity()).isEqualTo(50L);
|
||||
assertThat(result.records().get(0).totalQuantity()).isEqualTo(100L);
|
||||
}
|
||||
@Mock ReturnInventoryCartonMapper cartons;
|
||||
@Mock ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Mock ReturnInventoryBalanceMapper balances;
|
||||
@@ -60,6 +88,7 @@ class ReturnInventoryServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ReturnInventoryService();
|
||||
ReflectionTestUtils.setField(service, "skuConversions", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class));
|
||||
ReflectionTestUtils.setField(service, "cartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "cartonItems", cartonItems);
|
||||
ReflectionTestUtils.setField(service, "balances", balances);
|
||||
@@ -374,7 +403,7 @@ class ReturnInventoryServiceTest {
|
||||
@Test void completedLabelQuantityCannotBeGuessedWhenSplittingAStoredCarton() {
|
||||
prepareConcurrentSource();
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
|
||||
when(cartons.relabelledQuantity(31L, "SKU-A")).thenReturn(1L);
|
||||
when(cartons.completedSkuQuantity(31L, "SKU-A")).thenReturn(1L);
|
||||
var error = assertThrows(ResponseStatusException.class, () -> service.merge(
|
||||
new PdaWarehouseIdentity("operator-2", "name", 42L), mergeRequest(1)));
|
||||
assertThat(error.getReason()).contains("不能自动分配来源", "独立新箱");
|
||||
@@ -385,7 +414,7 @@ class ReturnInventoryServiceTest {
|
||||
@Test void wholeCartonMergeCarriesItsCompletedRelabelFactsToTheTarget() {
|
||||
var source = prepareConcurrentSource();
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
|
||||
when(cartons.relabelledQuantity(31L, "SKU-A")).thenReturn(2L);
|
||||
when(cartons.completedSkuQuantity(31L, "SKU-A")).thenReturn(2L);
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
var relabel = org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.mapper.RelabelItemMapper.class);
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package com.baiyue.lastleg.returnreceipt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.service.ReturnSkuConversionService;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.*;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
/** Real production SQL, contents, balances, provenance and rollback in one database transaction. */
|
||||
class ReturnSkuConversionPersistenceTest {
|
||||
JdbcTemplate jdbc;
|
||||
ReturnSkuConversionService service, target;
|
||||
ReturnInventoryBalanceMapper balances;
|
||||
DataSourceTransactionManager transactions;
|
||||
|
||||
@BeforeEach void setUp() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:sku-conversion-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds); transactions = new DataSourceTransactionManager(ds);
|
||||
for (var entity : List.of(ReturnInventoryCarton.class, ReturnInventoryCartonItem.class, ReturnInventoryBalance.class,
|
||||
ReturnSkuConversion.class, ReturnInventoryOperation.class, RelabelItem.class)) {
|
||||
var columns = Arrays.stream(entity.getDeclaredFields()).filter(f -> !Modifier.isStatic(f.getModifiers()))
|
||||
.filter(f -> f.getAnnotation(com.baomidou.mybatisplus.annotation.TableField.class) == null
|
||||
|| f.getAnnotation(com.baomidou.mybatisplus.annotation.TableField.class).exist())
|
||||
.map(f -> f.getName().replaceAll("([A-Z])", "_$1").toLowerCase() + " "
|
||||
+ (f.getType() == String.class ? "VARCHAR(8000)" : f.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
: f.getType() == java.math.BigDecimal.class ? "DECIMAL(20,6)" : "BIGINT")
|
||||
+ (f.getName().equals("id") ? " PRIMARY KEY" : "")).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")");
|
||||
}
|
||||
jdbc.execute("CREATE UNIQUE INDEX balance_sku ON ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code)");
|
||||
jdbc.execute("CREATE UNIQUE INDEX conversion_round ON ll_return_sku_conversion(item_id,round_key,old_sku_code,new_sku_code)");
|
||||
jdbc.execute("CREATE TABLE ll_return_packing_trace(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),operation_id BIGINT,source_carton_mark VARCHAR(128),target_carton_mark VARCHAR(128),return_no VARCHAR(64),sku_code VARCHAR(128),quantity BIGINT,created_at TIMESTAMP)");
|
||||
var config = new MybatisConfiguration(); config.setMapUnderscoreToCamelCase(true);
|
||||
for (var mapper : List.of(ReturnSkuConversionMapper.class, ReturnInventoryCartonMapper.class,
|
||||
ReturnInventoryCartonItemMapper.class, ReturnInventoryBalanceMapper.class, ReturnPackingTraceMapper.class,
|
||||
ReturnInventoryOperationMapper.class)) config.addMapper(mapper);
|
||||
var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(config);
|
||||
var session = new SqlSessionTemplate(factory.getObject());
|
||||
target = new ReturnSkuConversionService();
|
||||
ReflectionTestUtils.setField(target, "conversions", session.getMapper(ReturnSkuConversionMapper.class));
|
||||
ReflectionTestUtils.setField(target, "cartons", session.getMapper(ReturnInventoryCartonMapper.class));
|
||||
ReflectionTestUtils.setField(target, "contents", session.getMapper(ReturnInventoryCartonItemMapper.class));
|
||||
balances = session.getMapper(ReturnInventoryBalanceMapper.class);
|
||||
ReflectionTestUtils.setField(target, "balances", balances);
|
||||
ReflectionTestUtils.setField(target, "traces", session.getMapper(ReturnPackingTraceMapper.class));
|
||||
ReflectionTestUtils.setField(target, "operations", session.getMapper(ReturnInventoryOperationMapper.class));
|
||||
ReflectionTestUtils.setField(target, "json", new ObjectMapper());
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(transactions, new AnnotationTransactionAttributeSource()));
|
||||
service = (ReturnSkuConversionService) proxy.getProxy();
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"BOX", "PARCEL"})
|
||||
void partialConversionPreservesPhysicalUnitAndLocksUntilConfirmation(String type) {
|
||||
fixture(type); apply(1, 11, 8, "INITIAL");
|
||||
assertStock("A", 12, 0); assertStock("B", 8, 8);
|
||||
assertEquals(20, count("SELECT total_quantity FROM ll_return_inventory_carton WHERE id=101"));
|
||||
assertEquals(2, count("SELECT sku_types FROM ll_return_inventory_carton WHERE id=101"));
|
||||
assertEquals(12, balance("A", "total_quantity")); assertEquals(8, balance("B", "total_quantity"));
|
||||
assertEquals("BOX".equals(type) ? 1 : 0, balance("B", "carton_quantity"));
|
||||
var locked = balances.page("C", "W", "B", 0, 20).get(0);
|
||||
assertEquals(0, locked.getAvailableQuantity()); assertEquals(8, locked.getRelabelLockedQuantity());
|
||||
assertEquals(8, count("SELECT SUM(quantity) FROM ll_return_packing_trace WHERE target_carton_mark='UNIT-1' AND sku_code='B'"));
|
||||
service.confirm(app(1));
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',relabel_application_id=NULL WHERE id=101");
|
||||
assertEquals(8, balances.page("C", "W", "B", 0, 20).get(0).getAvailableQuantity());
|
||||
assertEquals("CONFIRMED", jdbc.queryForObject("SELECT status FROM ll_return_sku_conversion", String.class));
|
||||
}
|
||||
|
||||
@Test void replayAndLaterPartialRoundDoNotConvertPriorPiecesTwice() {
|
||||
fixture("BOX"); apply(1, 11, 8, "INITIAL"); apply(1, 11, 8, "INITIAL");
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_return_sku_conversion")); assertStock("A", 12, 0);
|
||||
service.confirm(app(1)); jdbc.update("UPDATE ll_return_inventory_carton SET relabel_application_id=2 WHERE id=101");
|
||||
apply(2, 12, 5, "INITIAL");
|
||||
assertStock("A", 7, 0); assertStock("B", 13, 13); assertEquals(20, balance("A", "total_quantity") + balance("B", "total_quantity"));
|
||||
var repeated = item(13, 2, "B", "C", 1);
|
||||
assertThrows(ResponseStatusException.class, () -> service.apply(app(2), List.of(repeated), List.of(fact(13, 1, "RELABELLED")), "INITIAL"));
|
||||
assertStock("B", 13, 13);
|
||||
}
|
||||
|
||||
@Test void fullConversionRemovesOldContentsButKeepsTheCartonAndOriginalReceipt() {
|
||||
fixture("BOX"); apply(1, 11, 20, "INITIAL");
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE sku_code='A'"));
|
||||
assertStock("B", 20, 20); assertEquals(0, balance("A", "total_quantity")); assertEquals(0, balance("A", "carton_quantity"));
|
||||
assertEquals(1, count("SELECT COUNT(*) FROM ll_return_inventory_carton WHERE carton_mark='UNIT-1' AND source_return_no='RT-1'"));
|
||||
}
|
||||
|
||||
@Test void conversionIntoExistingSkuKeepsItsUnlabelledPiecesSelectable() {
|
||||
fixture("BOX");
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton_item(id,carton_id,customer_id,sku_code,quantity,relabelled_quantity,packing_reserved_quantity) VALUES(202,101,'C','B',4,0,0)");
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET total_quantity=24,sku_types=2 WHERE id=101");
|
||||
balances.adjust(302, "C", "W", "B", "Goods", 4, 4, 1);
|
||||
apply(1, 11, 8, "INITIAL"); assertStock("B", 12, 8); assertEquals(1, balance("B", "carton_quantity"));
|
||||
assertEquals(4, count("SELECT quantity-relabelled_quantity FROM ll_return_inventory_carton_item WHERE sku_code='B'"));
|
||||
}
|
||||
|
||||
@Test void reworkReversesOnlyCurrentRoundAndCanBeAppliedAgain() {
|
||||
fixture("PARCEL"); apply(1, 11, 8, "INITIAL"); service.reverse(app(1)); service.reverse(app(1));
|
||||
assertStock("A", 20, 0); assertEquals(0, balance("B", "total_quantity"));
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE sku_code='B'"));
|
||||
apply(1, 11, 8, "REWORK-1"); assertStock("A", 12, 0); assertStock("B", 8, 8);
|
||||
assertEquals(2, count("SELECT COUNT(*) FROM ll_return_sku_conversion"));
|
||||
service.confirm(app(1)); assertThrows(ResponseStatusException.class, () -> service.reverse(app(1)));
|
||||
}
|
||||
|
||||
@Test void onlyActualCompletedFactsProduceInventoryAndBulkCountsPieces() {
|
||||
fixture("PARCEL"); var item = item(11, 1, "A", "B", 20); item.setOperationType("RETURN_BULK_SKU_LABEL");
|
||||
service.apply(app(1), List.of(item), List.of(fact(11, 8, "RELABELLED"), fact(11, 12, "EXCEPTION")), "INITIAL");
|
||||
assertStock("A", 12, 0); assertStock("B", 8, 8);
|
||||
}
|
||||
|
||||
@Test void balanceFailureRollsBackContentsLedgerAndOwnership() {
|
||||
fixture("BOX"); var failing = spy(balances);
|
||||
doReturn(0).when(failing).adjust(anyLong(), anyString(), anyString(), eq("B"), any(), anyLong(), anyLong(), anyLong());
|
||||
ReflectionTestUtils.setField(target, "balances", failing);
|
||||
assertThrows(ResponseStatusException.class, () -> apply(1, 11, 8, "INITIAL"));
|
||||
assertStock("A", 20, 0); assertEquals(20, balance("A", "total_quantity"));
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE sku_code='B'"));
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_return_sku_conversion"));
|
||||
assertEquals(0, count("SELECT COUNT(*) FROM ll_return_packing_trace"));
|
||||
}
|
||||
|
||||
@Test void manualCompletionOnlyConvertsSkuLinesActuallyScannedInCurrentRound() {
|
||||
fixture("BOX");
|
||||
var item = item(11,1,"A","B",8);
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"A\",\"newSkuCode\":\"B\",\"quantity\":8},{\"skuCode\":\"A\",\"newSkuCode\":\"C\",\"quantity\":4}]");
|
||||
var scan = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory(); scan.setAction("SKU_SCAN");
|
||||
scan.setDetailJson("{\"round\":\"CURRENT\",\"itemId\":\"11\",\"skuIndex\":0,\"oldSku\":\"A\",\"newSku\":\"B\",\"quantity\":8}");
|
||||
var previous = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory(); previous.setAction("SKU_SCAN");
|
||||
previous.setDetailJson("{\"round\":\"PREVIOUS\",\"itemId\":\"11\",\"skuIndex\":1,\"oldSku\":\"A\",\"newSku\":\"C\",\"quantity\":4}");
|
||||
service.apply(app(1), List.of(item), List.of(fact(11,1,"EXCEPTION")), "CURRENT", List.of(scan,previous));
|
||||
assertStock("A",12,0); assertStock("B",8,8);
|
||||
assertEquals(0,count("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE sku_code='C'"));
|
||||
}
|
||||
|
||||
@Test void foreignCustomerOrWarehouseCannotConvertStock() {
|
||||
fixture("BOX"); var foreign = app(1); foreign.setCustomerId("OTHER");
|
||||
assertThrows(ResponseStatusException.class, () -> service.apply(foreign, List.of(item(11,1,"A","B",8)), List.of(fact(11,1,"RELABELLED")), "INITIAL"));
|
||||
foreign.setCustomerId("C"); foreign.setOverseasWarehouseId("OTHER");
|
||||
assertThrows(ResponseStatusException.class, () -> service.apply(foreign, List.of(item(11,1,"A","B",8)), List.of(fact(11,1,"RELABELLED")), "INITIAL"));
|
||||
assertStock("A", 20, 0);
|
||||
}
|
||||
|
||||
void fixture(String type) {
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,carton_mark,source_return_no,source_unit_type,status,count_status,total_quantity,sku_types,relabel_application_id) VALUES(101,'C','W','UNIT-1','RT-1',?,'RELABEL_PENDING','CONFIRMED',20,1,1)",type);
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton_item(id,carton_id,customer_id,sku_code,product_name_zh,quantity,relabelled_quantity,packing_reserved_quantity) VALUES(201,101,'C','A','Goods',20,0,0)");
|
||||
balances.adjust(301,"C","W","A","Goods",20,20,"BOX".equals(type)?1:0);
|
||||
}
|
||||
void apply(long appId, long itemId, int quantity, String round) {
|
||||
service.apply(app(appId), List.of(item(itemId,appId,"A","B",quantity)), List.of(fact(itemId,1,"RELABELLED")), round);
|
||||
}
|
||||
RelabelApplication app(long id) { var app=new RelabelApplication(); app.setId(id); app.setCustomerId("C"); app.setOverseasWarehouseId("W"); app.setRelabelNo("RL-"+id); return app; }
|
||||
RelabelItem item(long id,long appId,String oldSku,String newSku,int quantity) {
|
||||
var item=new RelabelItem(); item.setId(id); item.setApplicationId(appId); item.setReturnCartonId(101L);
|
||||
item.setOperationType("SKU_LABEL"); item.setSkuCode(oldSku); item.setNewSkuCode(newSku);
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\""+oldSku+"\",\"newSkuCode\":\""+newSku+"\",\"quantity\":"+quantity+"}]"); return item;
|
||||
}
|
||||
PdaRelabelUnitFact fact(long itemId,int quantity,String result) { var f=new PdaRelabelUnitFact(); f.setItemId(itemId); f.setQuantity(quantity); f.setResult(result); return f; }
|
||||
long count(String sql) { return jdbc.queryForObject(sql,Long.class); }
|
||||
long balance(String sku,String column) { return jdbc.queryForObject("SELECT "+column+" FROM ll_return_inventory_balance WHERE sku_code=?",Long.class,sku); }
|
||||
void assertStock(String sku,long quantity,long labelled) {
|
||||
assertEquals(quantity,jdbc.queryForObject("SELECT quantity FROM ll_return_inventory_carton_item WHERE sku_code=?",Long.class,sku));
|
||||
assertEquals(labelled,jdbc.queryForObject("SELECT relabelled_quantity FROM ll_return_inventory_carton_item WHERE sku_code=?",Long.class,sku));
|
||||
}
|
||||
}
|
||||
+1
@@ -216,6 +216,7 @@ class WarehouseTransferRelabelPersistenceTest {
|
||||
when(tasks.lock("7",11L)).thenReturn(task);when(tasks.updateWithVersion(any(),any())).thenReturn(1);
|
||||
ReflectionTestUtils.setField(service,"pdaTasks",tasks);
|
||||
ReflectionTestUtils.setField(service,"returnOrders",mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service,"skuConversions",mock(com.baiyue.lastleg.relabel.service.ReturnSkuConversionService.class));
|
||||
ReflectionTestUtils.setField(service,"histories",mock(com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper.class));
|
||||
ReflectionTestUtils.setField(service,"orderHistories",mock(com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper.class));
|
||||
var orders=mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);var order=new com.baiyue.lastleg.entity.LastlegOrder();
|
||||
|
||||
Reference in New Issue
Block a user