This commit is contained in:
2026-10-19 17:59:34 +08:00
parent dcfba1053c
commit ce189f062f
14 changed files with 305 additions and 6 deletions
@@ -0,0 +1,81 @@
package com.baiyue.lastleg.discard;
import com.baiyue.lastleg.discard.entity.*;
import com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository;
import com.baiyue.lastleg.inventory.entity.*;
import com.baiyue.lastleg.inventory.mapper.*;
import com.baiyue.lastleg.returnreceipt.entity.*;
import com.baiyue.lastleg.returnreceipt.mapper.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** 校验按件、按箱扣减与总账事实的数量口径。 */
@ExtendWith(MockitoExtension.class)
class CargoDiscardStockLedgerRepositoryTest {
@Mock ReturnInventoryBalanceMapper returnBalances;
@Mock ReturnInventoryCartonMapper returnCartons;
@Mock InventoryBalanceMapper balances;
@Mock InventoryLedgerMapper ledgers;
CargoDiscardStockLedgerRepository repository;
/** 装配真实仓储逻辑及受控数据库边界。 */
@BeforeEach
void setup() {
repository = new CargoDiscardStockLedgerRepository();
ReflectionTestUtils.setField(repository, "returnBalances", returnBalances);
ReflectionTestUtils.setField(repository, "returnCartons", returnCartons);
ReflectionTestUtils.setField(repository, "balances", balances);
ReflectionTestUtils.setField(repository, "ledgers", ledgers);
}
/** 部分废弃保留箱数,整 SKU 耗尽减箱,包裹不占箱数。 */
@ParameterizedTest
@CsvSource({"BOX,10,5,0", "BOX,5,5,1", "PARCEL,5,5,0"})
void returnBalanceUsesPiecesAndRemainingSku(String unit, long before, long count, long boxes) {
var app = new CargoDiscardApplication(); app.setOverseasWarehouseId("W");
var item = new CargoDiscardItem(); item.setReturnCartonId(1L); item.setSkuCode("SKU");
var carton = new ReturnInventoryCarton(); carton.setCustomerId("C"); carton.setOverseasWarehouseId("W");
carton.setStatus("DISCARD_PENDING"); carton.setSourceUnitType(unit);
when(returnCartons.lockId(1L)).thenReturn(carton);
when(returnBalances.lockSku("C", "W", "SKU")).thenReturn(new ReturnInventoryBalance());
when(returnBalances.deductConversion("C", "W", "SKU", count, boxes)).thenReturn(1);
repository.deductReturn("C", app, item, before, count);
verify(returnBalances).deductConversion("C", "W", "SKU", count, boxes);
}
/** 其他客户的退货箱不能扣当前客户库存。 */
@Test
void rejectsForeignCartonBeforeBalanceMutation() {
var item = new CargoDiscardItem(); item.setReturnCartonId(1L);
var carton = new ReturnInventoryCarton(); carton.setCustomerId("OTHER");
when(returnCartons.lockId(1L)).thenReturn(carton);
assertThrows(ResponseStatusException.class, () -> repository.deductReturn("C", new CargoDiscardApplication(), item, 5, 5));
verifyNoInteractions(returnBalances);
}
/** 一件代发混装箱只按实物箱数记总账,并关联废弃业务单。 */
@Test
void dropshipLedgerRecordsPhysicalBoxesAndBusinessReference() {
var app = new CargoDiscardApplication(); app.setId(10L); app.setDiscardNo("CD-1");
var item = new CargoDiscardItem(); item.setId(20L); item.setSkuCode("MIXED");
var position = new InventoryPosition(); position.setId(30L); position.setBalanceId(40L); position.setPalletNo("P1");
var balance = new InventoryBalance(); balance.setId(40L); balance.setCustomerId("C"); balance.setAvailablePackages(9L);
when(balances.selectById(40L)).thenReturn(balance); when(ledgers.insert(any(InventoryLedger.class))).thenReturn(1);
repository.recordDropship("C", app, item, position, 1, "operator", LocalDateTime.now());
var fact = ArgumentCaptor.forClass(InventoryLedger.class); verify(ledgers).insert(fact.capture());
assertEquals(10L, fact.getValue().getBeforePackages()); assertEquals(-1L, fact.getValue().getDeltaPackages());
assertEquals(9L, fact.getValue().getAfterPackages()); assertEquals("CD-1", fact.getValue().getBusinessOrderNo());
assertEquals("DISCARD", fact.getValue().getBusinessType()); assertEquals("P1", fact.getValue().getPalletNo());
}
}
@@ -0,0 +1,55 @@
package com.baiyue.lastleg.discard;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnDiscardMovementMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryMovementMapper;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
/** 用真实 SQL 验证完成事实、历史兼容及客户箱号隔离。 */
class ReturnDiscardMovementMapperTest {
/** 历史已完成废弃展示负件数;未完成、其他客户及其他仓记录均排除。 */
@Test
void completedFactsAreVisibleInMovementsAndCartonHistory() {
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
var db = new JdbcTemplate(source);
db.execute("CREATE TABLE ll_cargo_discard_application(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),discard_no VARCHAR(30))");
db.execute("CREATE TABLE ll_return_inventory_carton(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
db.execute("ALTER TABLE ll_return_inventory_carton ADD carton_mark VARCHAR(30), ADD source_batch_no VARCHAR(30), ADD created_at TIMESTAMP");
db.execute("CREATE TABLE ll_return_inventory_carton_item(carton_id BIGINT,sku_code VARCHAR(30),product_name_zh VARCHAR(30),quantity BIGINT)");
db.execute("CREATE TABLE ll_return_receipt(id BIGINT,return_order_id BIGINT,receipt_no VARCHAR(30),signed_at TIMESTAMP)");
db.execute("CREATE TABLE ll_return_order(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
db.execute("CREATE TABLE ll_return_receipt_item(receipt_id BIGINT,sku_code VARCHAR(30),return_order_item_id BIGINT,received_quantity BIGINT)");
db.execute("CREATE TABLE ll_return_order_item(id BIGINT,product_name_zh VARCHAR(30))");
db.execute("CREATE TABLE ll_return_inventory_outbound_movement(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),sku_code VARCHAR(30),product_name_zh VARCHAR(30),outbound_no VARCHAR(30),quantity BIGINT,occurred_at TIMESTAMP)");
db.execute("CREATE TABLE ll_return_carton_stock_receipt(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),sku_code VARCHAR(30),product_name_zh VARCHAR(30),receipt_no VARCHAR(30),quantity BIGINT,received_at TIMESTAMP)");
db.execute("CREATE TABLE ll_return_sku_conversion(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),old_sku_code VARCHAR(30),new_sku_code VARCHAR(30),product_name_zh VARCHAR(30),relabel_no VARCHAR(30),quantity BIGINT,completed_at TIMESTAMP,reversed_at TIMESTAMP,status VARCHAR(30))");
db.execute("CREATE TABLE ll_cargo_discard_item(id BIGINT,application_id BIGINT,customer_id VARCHAR(20),return_carton_id BIGINT,sku_code VARCHAR(30),product_name VARCHAR(30),pda_status VARCHAR(20),available_packages_snapshot INT,actual_discard_packages INT,pda_operator_id VARCHAR(20),pda_completed_at TIMESTAMP)");
db.update("INSERT INTO ll_cargo_discard_application VALUES(1,'C','W','CD-1'),(2,'OTHER','W','CD-2'),(3,'C','OTHER-W','CD-3')");
db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id) VALUES(10,'C','W'),(20,'OTHER','W')");
db.update("INSERT INTO ll_cargo_discard_item VALUES(1,1,'C',10,'SKU','货物','COMPLETED',10,5,'OP',CURRENT_TIMESTAMP),(2,1,'C',10,'SKU','货物','PENDING',10,5,NULL,NULL),(3,2,'OTHER',20,'SKU','货物','COMPLETED',5,5,'OP',CURRENT_TIMESTAMP),(4,3,'C',10,'SKU','货物','COMPLETED',5,5,'OP',CURRENT_TIMESTAMP)");
var config = new Configuration(new Environment("test", new JdbcTransactionFactory(), source));
config.addMapper(ReturnDiscardMovementMapper.class);
config.addMapper(ReturnInventoryMovementMapper.class);
try (var session = new SqlSessionFactoryBuilder().build(config).openSession()) {
var mapper = session.getMapper(ReturnDiscardMovementMapper.class);
var history = mapper.cartonHistory("C", 10L);
assertEquals(1, history.size()); assertEquals("CD-1", history.get(0).discardNo());
assertEquals(5, history.get(0).quantity()); assertEquals(5, history.get(0).afterQuantity());
assertTrue(mapper.cartonHistory("OTHER", 10L).isEmpty());
assertEquals(2, db.queryForObject("SELECT COUNT(*) FROM (" + ReturnDiscardMovementMapper.MOVEMENTS + ") m", Integer.class));
var inventory = session.getMapper(ReturnInventoryMovementMapper.class);
assertEquals(1, inventory.platformCount("C", "W", "SKU"));
var movement = inventory.platformPage("C", "W", "SKU", 0L, 20L).get(0);
assertEquals("DISCARD", movement.movementType()); assertEquals("CD-1", movement.referenceNo());
assertEquals(-5, movement.totalDelta()); assertEquals(-5, movement.availableDelta());
assertEquals(0, inventory.platformCount("C", "W", "MISSING-SKU"));
}
}
}
@@ -50,11 +50,15 @@ class CargoDiscardInventoryAdjustmentServiceTest {
@Mock DropshipOrderProfileMapper profiles;
@Mock InventoryPositionMapper positions;
@Mock InventoryBalanceMapper balances;
@Mock com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository stockLedger;
@Mock com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper returnItems;
private CargoDiscardInventoryAdjustmentService service;
@BeforeEach
void setUp() {
service = new CargoDiscardInventoryAdjustmentService();
ReflectionTestUtils.setField(service, "stockLedger", stockLedger);
ReflectionTestUtils.setField(service, "returnCartonItems", returnItems);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "cartonLedgers", ledgers);
ReflectionTestUtils.setField(service, "cartonSkus", cartonSkus);
@@ -64,6 +68,21 @@ class CargoDiscardInventoryAdjustmentServiceTest {
ReflectionTestUtils.setField(service, "balances", balances);
}
/** 退货来源按 SKU 件数同时扣箱明细和客户库存汇总。 */
@Test
void returnDiscardSynchronizesCartonAndBalance() {
var app = application(1);
var line = item(5); line.setSourceType("RETURN_CARTON"); line.setReturnCartonId(31L);
var content = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
content.setId(32L); content.setCustomerId("100"); content.setQuantity(5L);
when(returnItems.lockSku(31L, "SKU-1")).thenReturn(content);
when(returnItems.deduct(32L, 5)).thenReturn(1);
var result = service.discardPalletItem("100", app, line, List.of(), 5, "OP", LocalDateTime.now());
assertEquals(5, result.discardedPackages()); assertEquals(0, result.afterPackages());
verify(stockLedger).deductReturn("100", app, line, 5L, 5L);
org.mockito.Mockito.verifyNoInteractions(positions, balances, ledgers);
}
@Test
void pdaDiscardDeductsEverySkuInsideOnePhysicalCartonOnce() {
CargoDiscardApplication application = application(1);
@@ -96,6 +115,7 @@ class CargoDiscardInventoryAdjustmentServiceTest {
verify(skuInventories).deduct("100", 201L, "SKU-A", 1, 2);
verify(skuInventories).deduct("100", 201L, "SKU-B", 1, 3);
verify(positions).deductAvailable("100", 801L, 1);
verify(stockLedger).recordDropship(eq("100"), eq(application), eq(item), eq(position), eq(1L), eq("operator"), any());
}
@Test
@@ -88,6 +88,7 @@ class ReturnInventoryServiceTest {
@BeforeEach
void setUp() {
service = new ReturnInventoryService();
ReflectionTestUtils.setField(service, "discardMovements", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.repository.ReturnDiscardMovementRepository.class));
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);