This commit is contained in:
2026-10-29 15:49:18 +08:00
parent 849f8dc5f2
commit 0bd25de906
23 changed files with 293 additions and 16 deletions
@@ -38,7 +38,7 @@ class PdaCargoDiscardServiceTest {
private CargoDiscardItem item;
private final PdaWarehouseIdentity identity = new PdaWarehouseIdentity("OP", "operator", 99L);
private final com.baiyue.lastleg.discard.service.CargoDiscardPhotoService photoService = mock(com.baiyue.lastleg.discard.service.CargoDiscardPhotoService.class);
private PdaCargoDiscardDtos.Confirm request(int actual) { return new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, actual)), List.of(101L)); }
private PdaCargoDiscardDtos.Confirm request(int actual) { return new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, actual, marks(11L, actual))), List.of(101L)); }
@BeforeEach void setup() {
service = new PdaCargoDiscardService();
@@ -162,9 +162,55 @@ class PdaCargoDiscardServiceTest {
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(actual)));
noWrites(); verifyNoInteractions(photoService);
}
/** 数量申请可仅使用一个托盘,也可跨托盘完成,剩余预留只释放一次。 */
@ParameterizedTest @ValueSource(ints = {0, 2})
void quantityTargetUsesChosenPalletsAndReleasesUnusedReservations(int firstActual) {
var stock = mock(com.baiyue.lastleg.discard.service.CargoDiscardStockService.class);
ReflectionTestUtils.setField(service, "stockService", stock); scannedStock(stock);
application.setTotalPackages(5); application.setRemark("只废弃破损外箱"); application.setStatus(CargoDiscardService.APPROVED);
item.setSourceType("INVENTORY"); item.setInventoryPositionId(42L); item.setDiscardPackages(12);
var second = new CargoDiscardItem(); second.setId(12L); second.setCustomerId("CUSTOMER"); second.setApplicationId(1L);
second.setSourceType("INVENTORY"); second.setInventoryPositionId(43L); second.setSkuCode("SKU"); second.setDiscardPackages(10); second.setPdaStatus("PENDING");
when(items.listApplication("CUSTOMER", 1L)).thenAnswer(call -> List.of(item, second));
when(items.selectById(12L)).thenReturn(second);
for (var line : List.of(item, second)) {
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition(); position.setOverseasWarehouseId("99");
position.setPalletNo(line == item ? "PALLET-1" : "PALLET-2");
when(stock.position("CUSTOMER", line.getInventoryPositionId())).thenReturn(position);
when(stock.actualQuantityEditable(line)).thenReturn(true);
}
when(applications.selectList(any())).thenReturn(List.of(application));
var task = service.resolve(identity, "PALLET-2", "CD-1");
assertEquals("只废弃破损外箱", task.remark()); assertEquals(5, task.requestedPackages()); assertEquals(5, task.remainingPackages());
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(6)));
verifyNoInteractions(photoService);
if (firstActual > 0) {
service.confirm(identity, "CD-1", "PALLET-1", request(firstActual));
assertEquals(CargoDiscardService.APPROVED, application.getStatus());
assertEquals(3, service.resolve(identity, "PALLET-2", "CD-1").remainingPackages());
}
var secondRequest = new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(12L, 5-firstActual, marks(12L, 5-firstActual))), List.of(101L));
if (firstActual > 0) assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-2",
new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(12L, 3, List.of("FIELD-11-0", "SECOND-1", "SECOND-2"))), List.of(101L))));
service.confirm(identity, "CD-1", "PALLET-2", secondRequest);
assertEquals(CargoDiscardService.PDA_COMPLETED, application.getStatus()); assertEquals(5, application.getTotalPackages());
verify(stock).discard("CUSTOMER", "OP", application, second, 5-firstActual);
verify(stock).discard("CUSTOMER", "OP", application, item, firstActual);
service.confirm(identity, "CD-1", "PALLET-2", secondRequest);
verify(stock, times(1)).discard("CUSTOMER", "OP", application, second, 5-firstActual);
verifyNoInteractions(adjustments);
}
/** 生成现场扫码样本,各托盘箱唛独立。 */
private List<String> marks(long itemId, int count) { return java.util.stream.IntStream.range(0, count).mapToObj(i -> "FIELD-" + itemId + "-" + i).toList(); }
/** Mock仍执行真实JSON序列化,验证重放时箱唛不可篡改。 */
private void scannedStock(com.baiyue.lastleg.discard.service.CargoDiscardStockService stock) {
ReflectionTestUtils.setField(stock, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
doCallRealMethod().when(stock).saveActualCartonMarks(any(), anyList());
when(stock.actualCartonMarks(any())).thenCallRealMethod();
}
private void looseTask() {
var stockService = mock(com.baiyue.lastleg.discard.service.CargoDiscardStockService.class);
ReflectionTestUtils.setField(service, "stockService", stockService);
ReflectionTestUtils.setField(service, "stockService", stockService); scannedStock(stockService);
item.setSourceType("INVENTORY"); item.setInventoryPositionId(42L); item.setDiscardPackages(2);
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition();
position.setId(42L); position.setCustomerId("CUSTOMER"); position.setOverseasWarehouseId("99");
@@ -180,6 +226,7 @@ class PdaCargoDiscardServiceTest {
void unboundPalletUsesActualQuantityForInventoryFeeAndReplay(int actual) {
var stockService = mock(com.baiyue.lastleg.discard.service.CargoDiscardStockService.class);
ReflectionTestUtils.setField(service, "stockService", stockService);
scannedStock(stockService);
item.setSourceType("INVENTORY"); item.setInventoryPositionId(42L); item.setDiscardPackages(3);
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition();
position.setOverseasWarehouseId("99"); position.setPalletNo("PALLET-1");
@@ -190,13 +237,21 @@ class PdaCargoDiscardServiceTest {
var task = service.resolve(identity, "PALLET-1", "CD-1");
assertTrue(task.skus().get(0).actualQuantityEditable()); assertEquals("PALLET", task.scanMode());
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(4)));
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1",
new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, 1)), List.of(101L))));
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1",
new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, 2, List.of("SAME", "SAME"))), List.of(101L))));
verifyNoInteractions(photoService);
var completed = service.confirm(identity, "CD-1", "PALLET-1", request(actual));
assertEquals(actual, completed.skus().get(0).actualPackages());
assertEquals(actual, application.getTotalPackages());
assertEquals(new BigDecimal("5.00").multiply(BigDecimal.valueOf(actual)), application.getTotalFee());
assertEquals(marks(11L, actual), stockService.actualCartonMarks(item));
service.confirm(identity, "CD-1", "PALLET-1", request(actual));
verify(stockService, times(1)).discard("CUSTOMER", "OP", application, item, actual);
if (actual > 0) assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1",
new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, actual,
java.util.stream.IntStream.range(0, actual).mapToObj(i -> "CHANGED-" + i).toList())), List.of(101L))));
verifyNoInteractions(adjustments);
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(3)));
verify(items, times(1)).updateById(item);
@@ -142,6 +142,31 @@ class RelabelApplicationServiceTest {
assertEquals(0, occupied.cartonAvailablePackages());
assertEquals(2, occupied.skuItems().size(), "处理中库存保留占用信息,但不可再次选择");
}
@Test void sharedCandidatesExcludeDropshipButKeepOrdinaryInbound() {
ReflectionTestUtils.setField(service, "returnCartons", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class));
when(orders.listDropshipInboundCodes("10001", "WH-1")).thenReturn(List.of("DROP-1"));
var excluded = new InventoryBalance(); excluded.setId(1L); excluded.setInboundCode("DROP-1");
var ordinary = new InventoryBalance(); ordinary.setId(2L); ordinary.setInboundCode("NORMAL-1");
when(inventoryBalances.selectList(any())).thenReturn(List.of(excluded, ordinary));
var position = new InventoryPosition(); position.setId(3L); position.setBalanceId(2L); position.setAvailablePackages(4L);
when(inventoryPositions.listBalances("10001", List.of(2L))).thenReturn(List.of(position));
var result = service.candidates("10001", "WH-1", null);
assertEquals(List.of("NORMAL-1"), result.stream().map(RelabelResponses.InventoryCandidate::inboundCode).toList());
}
@Test void staleDropshipSelectionCannotCreateRelabelApplication() {
when(orders.listDropshipInboundCodes("10001", "WH-1")).thenReturn(List.of("CODE-1"));
var error = assertThrows(ResponseStatusException.class,
() -> service.create("10001", "operator-1", request("dropship", 1, 1)));
assertEquals("一件代发订单不支持贴标申请", error.getReason());
verify(applications, never()).insert(any(RelabelApplication.class));
verifyNoInteractions(inventoryCommands);
}
@Test void convertedSkuRemainsAvailableForCartonLabelOnly() {
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);
@@ -245,6 +270,7 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "walletCommands", walletCommands);
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
ReflectionTestUtils.setField(service, "sourceRepository", new com.baiyue.lastleg.relabel.repository.RelabelSourceRepository(orders));
ReflectionTestUtils.setField(service, "inventoryPositions", inventoryPositions);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
@@ -0,0 +1,31 @@
package com.baiyue.lastleg.relabel.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.relabel.repository.RelabelSourceRepository;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import static org.assertj.core.api.Assertions.assertThat;
/** 用真实 Mapper 验证业务类型、客户和仓库隔离,避免跨来源误隐藏。 */
class RelabelSourceRepositoryTest {
@Test void onlyDropshipSourcesInCurrentCustomerAndWarehouseAreExcluded() throws Exception {
var ds = new DriverManagerDataSource("jdbc:h2:mem:relabel_sources;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
var jdbc = new JdbcTemplate(ds);
jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),order_type VARCHAR(64),inbound_code VARCHAR(64))");
jdbc.execute("INSERT INTO ll_order VALUES (1,'C1','W1','ONE_CLICK_DROPSHIP','DROP'),(2,'C1','W1','WAREHOUSE_TRANSFER','NORMAL'),(3,'C2','W1','ONE_CLICK_DROPSHIP','FOREIGN'),(4,'C1','W2','ONE_CLICK_DROPSHIP','OTHER-WAREHOUSE')");
var configuration = new MybatisConfiguration();
configuration.addMapper(LastlegOrderMapper.class);
var factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(ds); factory.setConfiguration(configuration);
var repository = new RelabelSourceRepository(new SqlSessionTemplate(factory.getObject()).getMapper(LastlegOrderMapper.class));
assertThat(repository.dropshipInboundCodes("C1", "W1")).containsExactly("DROP");
assertThat(repository.dropshipInboundCodes("C2", "W1")).containsExactly("FOREIGN");
assertThat(repository.dropshipInboundCodes("C1", "W2")).containsExactly("OTHER-WAREHOUSE");
}
}
@@ -1283,6 +1283,20 @@ class WarehouseTransferServiceTest {
verify(orders, never()).insert(any(LastlegOrder.class));
}
@Test
void staleDropshipSelectionCannotCreateWarehouseTransfer() {
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
var dropship = historicalOrder(); dropship.setOrderType("ONE_CLICK_DROPSHIP");
when(orders.selectList(any())).thenReturn(List.of(dropship));
var error = assertThrows(ResponseStatusException.class,
() -> service.create("TENANT-A", "USER-1", request(false)));
assertEquals("一件代发订单不支持仓储转运", error.getReason());
verify(transfers, never()).insert(any(WarehouseTransferOrder.class));
verifyNoInteractions(inventory);
}
@Test
void missingOrForeignCustomerNameStopsCreationBeforeAnyBusinessWrites() {
for (var customer : List.of(