Merge remote-tracking branch 'origin/test' into test

This commit is contained in:
2026-10-14 14:01:40 +08:00
13 changed files with 408 additions and 11 deletions
@@ -5,6 +5,8 @@ import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.List;
@@ -187,6 +189,17 @@ class DispatchCandidateMapperContractTest {
}
}
@Test
void relabelCandidatesReadWeightFromWarehouseTransferPalletSnapshot() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java"));
int start = source.indexOf("List<DispatchPalletCandidate> relabelAvailable");
int end = source.indexOf("List<DispatchPalletCandidate> nativeReturnRelabelAvailable");
String sql = source.substring(source.lastIndexOf("@Select", start), end);
assertTrue(sql.contains("MAX(wtp.gross_weight_kg) AS grossWeightKg"));
assertTrue(sql.contains("MAX(wtp.volume_cbm) AS volumeCbm"));
assertTrue(sql.contains("wtp.warehouse_pallet_id=pos.pallet_id"));
}
@Test
void candidateGrossWeightUsesPhysicalSourcesAndNeverBillingVolumeWeight() throws Exception {
for (Method method : List.of(
@@ -25,6 +25,11 @@ class DispatchPhysicalWeightTest {
int to = sql.indexOf(" AS grossWeightKg,");
if (from < 0 || to < 0) continue;
String projection = sql.substring(from + "AS volumeWeightKg,".length(), to);
if (projection.contains("wtp.gross_weight_kg")) {
assertThat(jdbc.queryForObject("SELECT " + projection + " FROM (SELECT CAST(NULL AS DECIMAL(12,3)) gross_weight_kg) wtp", BigDecimal.class)).isNull();
assertThat(jdbc.queryForObject("SELECT " + projection + " FROM (SELECT 80 gross_weight_kg) wtp", BigDecimal.class)).isEqualByComparingTo("80");
continue;
}
String query = "SELECT " + projection + " FROM (SELECT '10' AS customer_id) ip "
+ "CROSS JOIN (SELECT 1 AS order_id,10 AS volume_cbm) l "
+ "CROSS JOIN (SELECT 1 AS line_id,5 AS allocated_volume_cbm) a "
@@ -65,11 +65,13 @@ class DispatchServiceTest {
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
@Mock ReturnRelabelDispatchStock returnRelabelStock;
@Mock com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService relabelMeasurements;
private DispatchService service;
@BeforeEach
void setUp() {
service = new DispatchService();
ReflectionTestUtils.setField(service, "relabelMeasurements", relabelMeasurements);
ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow);
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
ReflectionTestUtils.setField(service, "returnRelabelStock", returnRelabelStock);
@@ -28,7 +28,7 @@ class PdaPalletOperationControllerContractTest {
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
assertEquals(Set.of("/pallets/resolve", "/relabel-pallet-label", "/assemble", "/disassemble",
"/relabel-disassemble", "/relabel-sources", "/swap", "/unbind"), paths);
"/relabel-disassemble", "/relabel-sources", "/relabel-pallet-measure", "/relabel-pallet-measurements", "/swap", "/unbind"), paths);
}
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
@@ -138,14 +138,14 @@ class PdaPalletOperationServiceTest {
new PalletOperationRequests.PalletExpectation("SOURCE", 1),
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("BOX-1"), "rebind");
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request, false));
verifyNoInteractions(physical);
var binding = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
binding.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"BOX-1\"]}");
when(workHistory.selectList(any())).thenReturn(List.of(binding));
task.setReviewedAt(LocalDateTime.of(2026, 10, 12, 12, 0));
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request, false));
task.setReviewedAt(null);
when(physical.resolveLocation(88L, 1020L)).thenReturn(
new PalletPhysicalGateway.LocationReference(1020L, "SOURCE-LOC", "STORAGE", "ACTIVE"));
@@ -155,7 +155,7 @@ class PdaPalletOperationServiceTest {
when(physical.resolve("TARGET")).thenReturn(target);
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation)
ReflectionTestUtils.invokeMethod(service, "bindReturnRelabelPallet", identity(), app,
List.of(item), source, target, request);
List.of(item), source, target, request, false);
assertEquals("TARGET", box.getPalletCode());
assertEquals(0, result.pallets().get(0).totalPackages());
assertEquals(2, result.pallets().get(1).totalPackages());
@@ -584,6 +584,35 @@ class PdaPalletOperationServiceTest {
assertThrows(ResponseStatusException.class, () -> service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "different")));
}
@Test void wholeRelabelCanRetainOriginalPalletWithoutMovingStockAndPartialCannot() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
ReflectionTestUtils.setField(service,"relabelWorkTasks",tasks); ReflectionTestUtils.setField(service,"relabelWorkHistory",histories);
var app = new RelabelApplication();app.setId(600L);app.setCustomerId("100");app.setRelabelNo("RL-1");
app.setOverseasWarehouseId("88");app.setStatus("READY_FOR_OUTBOUND");app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(app);
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();task.setId(900L);task.setStatus("COMPLETED");
when(tasks.find("100",600L)).thenReturn(task);
var item = relabelItem(701L,"CARTON-A",100L);
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(relabelApplications.selectById(600L)).thenReturn(app);
when(physical.resolve("SOURCE")).thenReturn(pallet(10L,"SOURCE",88L,0));
when(physical.resolveLocation(88L,1010L)).thenReturn(new PalletPhysicalGateway.LocationReference(1010L,"LOC-1010","STORAGE","ACTIVE"));
var position = position(100L,10L,"SOURCE",1,0);when(positions.selectById(100L)).thenReturn(position);
var row = row(100L,10L,0,0);row.setHoldPackages(1L);
when(mapper.lockPosition("88",100L)).thenReturn(row);when(mapper.content("88",10L)).thenReturn(List.of(row));
when(mapper.palletPackages("88",10L)).thenReturn(1L);
var request = new PalletOperationRequests.RelabelDisassemble("retain-1","RL-1",
new PalletOperationRequests.PalletExpectation("SOURCE",0),new PalletOperationRequests.PalletExpectation("SOURCE",0),List.of("CARTON-A"),"整托贴标");
var result = service.relabelDisassemble(identity(),request);
assertEquals("COMPLETED",result.status()); assertEquals(1,result.pallets().size());
verify(positions,never()).updateById(any(InventoryPosition.class)); verifyNoInteractions(relabelPalletMutations,ledgers);
verify(histories).insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class));
when(mapper.palletPackages("88",10L)).thenReturn(2L);
assertTrue(assertThrows(ResponseStatusException.class,() -> service.relabelDisassemble(identity(),request)).getReason().contains("部分贴标"));
}
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,
@@ -0,0 +1,86 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.relabel.mapper.*;
import com.baiyue.lastleg.relabel.entity.*;
import com.baiyue.lastleg.relabel.pda.entity.*;
import com.baiyue.lastleg.relabel.pda.mapper.*;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.*;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class RelabelPalletMeasurementServiceTest {
final RelabelPalletMeasurementService service = new RelabelPalletMeasurementService();
final RelabelPalletMeasurementMapper mapper = mock(RelabelPalletMeasurementMapper.class);
final RelabelApplicationMapper apps = mock(RelabelApplicationMapper.class);
final RelabelItemMapper items = mock(RelabelItemMapper.class);
final PdaRelabelHistoryMapper histories = mock(PdaRelabelHistoryMapper.class);
final PdaRelabelTaskMapper tasks = mock(PdaRelabelTaskMapper.class);
final RelabelApplication app = new RelabelApplication();
final List<PdaRelabelHistory> records = new ArrayList<>();
final ObjectMapper json = new ObjectMapper();
@BeforeEach void setup() {
ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"applications",apps);
ReflectionTestUtils.setField(service,"items",items); ReflectionTestUtils.setField(service,"histories",histories);
ReflectionTestUtils.setField(service,"tasks",tasks); ReflectionTestUtils.setField(service,"json",json);
app.setId(1L);app.setCustomerId("7");app.setRelabelNo("RL-1");app.setOverseasWarehouseId("92");
app.setStatus("READY_FOR_OUTBOUND");app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
when(apps.findAnyByRelabelNo("RL-1")).thenReturn(app);when(apps.lockAnyByRelabelNo("RL-1")).thenReturn(app);
when(mapper.transfer("7","RL-1")).thenReturn(10L);
when(mapper.originals("7",10L)).thenReturn(List.of(new RelabelPalletMeasurementMapper.Original(100L,"OLD",10,
new BigDecimal("100"),new BigDecimal("100"),new BigDecimal("100"),new BigDecimal("80"),BigDecimal.ONE)));
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",10,1)));
when(items.selectList(any())).thenReturn(List.of());
when(histories.selectList(any())).thenAnswer(i -> List.copyOf(records));
when(histories.insert(any(PdaRelabelHistory.class))).thenAnswer(i -> { records.add(i.getArgument(0));return 1; });
var task = new PdaRelabelTask(); task.setId(2L); task.setStatus("COMPLETED");
when(tasks.find("7",1L)).thenReturn(task);
}
RelabelPalletMeasurementMapper.Stock stock(long id,long pallet,String code,long count,long version) {
return new RelabelPalletMeasurementMapper.Stock(id,pallet,code,count,version);
}
@Test void wholeOriginalPalletInheritsWithoutInventingOrRescalingMeasurements() {
var p = service.snapshots(app).get(0);assertTrue(p.inherited()); assertEquals(new BigDecimal("80"),p.grossWeightKg());
assertEquals(BigDecimal.ONE,p.volumeCbm());assertDoesNotThrow(() -> service.requireMeasured(app));
}
@Test void partialSplitRequiresBothNewPalletAndOriginalRemainderMeasurements() throws Exception {
split(); var values = service.snapshots(app);assertEquals(2,values.size());
assertTrue(values.stream().allMatch(p -> p.volumeCbm()==null && p.grossWeightKg()==null));
assertThrows(ResponseStatusException.class,() -> service.requireMeasured(app));
}
@Test void explicitFinalMeasurementsAreAuditedAndReplayDoesNotDuplicateFacts() throws Exception {
split();
for (var p : service.snapshots(app)) {
var request = new RelabelPalletMeasurementService.Measure("RL-1",p.palletCode(),"measure-"+p.palletId(),p.stockVersion(),
new BigDecimal("100"),new BigDecimal("50"),new BigDecimal("50"),new BigDecimal("40"));
service.save(identity(),request);service.save(identity(),request);
}
assertEquals(3,records.size()); // One binding, two measurements.
assertDoesNotThrow(() -> service.requireMeasured(app));
assertTrue(service.snapshots(app).stream().allMatch(p -> p.volumeCbm().compareTo(new BigDecimal("0.25"))==0));
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",5,3),stock(2,200,"NEW",5,1)));
assertThrows(ResponseStatusException.class,() -> service.requireMeasured(app));
}
@Test void staleAndForeignMeasurementsAndAllocatedOrdersAreRejected() {
var request = new RelabelPalletMeasurementService.Measure("RL-1","OLD","key","stale",
BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE);
assertThrows(ResponseStatusException.class,() -> service.save(identity(),request));
app.setOverseasWarehouseId("93"); assertThrows(ResponseStatusException.class,() -> service.list(identity(),"RL-1"));
app.setOverseasWarehouseId("92");app.setFulfillmentStatus("PRE_DISPATCH_CREATED");
assertThrows(ResponseStatusException.class,() -> service.save(identity(),request));
verify(histories,never()).insert(any(PdaRelabelHistory.class));
}
private void split() throws Exception {
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",5,2),stock(2,200,"NEW",5,1)));
when(mapper.movements("7",100L)).thenReturn(1);
var binding = new PdaRelabelHistory();binding.setAction("PALLET_BIND");
binding.setDetailJson(json.writeValueAsString(Map.of("sourcePallets",List.of("OLD"),"targetPallets",List.of("NEW")))); records.add(binding);
}
private PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("operator","现场人员",92L); }
}