This commit is contained in:
2026-11-04 15:55:55 +08:00
parent fbdf58c23c
commit 0993dd446e
11 changed files with 328 additions and 10 deletions
@@ -82,6 +82,31 @@ class StorageRentServiceTest {
var fee=new OverseasFeeSource();fee.setSourceFeeId(701L);when(fees.publishSettled(anyString(),anyString(),any(),any(),any())).thenReturn(fee);
}
/** 同数量同金额也必须核对原箱身份,实测体积变化不能继续沿用冻结。 */
@ParameterizedTest @ValueSource(booleans={false,true})
void transferVolumeOrCartonChangeInvalidatesFreeze(boolean changeVolume) {
var a=plan.allocations().get(0);
plan=new Plan(plan.owner(),List.of(new Allocation(a.lotId(),a.shareId(),a.quantity(),a.amount(),a.inboundAt(),a.days(),null,new BigDecimal("0.036"),List.of("LARGE"))),plan.calculatedAt(),plan.amount());
ready();
plan=new Plan(plan.owner(),List.of(new Allocation(a.lotId(),a.shareId(),a.quantity(),a.amount(),a.inboundAt(),a.days(),null,
new BigDecimal(changeVolume?"0.035":"0.036"),List.of(changeVolume?"LARGE":"OTHER"))),plan.calculatedAt(),plan.amount());
assertThatThrownBy(()->service.consume("9",request)).hasMessageContaining("计费时间或货物已变化");
assertThat(lot.getRemainingQuantity()).isEqualByComparingTo("10");
verifyNoInteractions(fees);
}
/** 成功消费实箱后同事务保存离仓状态,后续扫码不能再次使用这些箱计费。 */
@Test void transferConsumptionPersistsCartonIdentityWithOriginalVolume() {
var a=plan.allocations().get(0);lot.setBusinessId(81L);
plan=new Plan(plan.owner(),List.of(new Allocation(a.lotId(),a.shareId(),a.quantity(),a.amount(),a.inboundAt(),a.days(),null,
new BigDecimal("0.036"),List.of("LARGE"))),plan.calculatedAt(),plan.amount());
when(repository.shipTransferRentCartons("101",81L,List.of("LARGE"))).thenReturn(1);
ready();service.consume("9",request);
verify(repository).shipTransferRentCartons("101",81L,List.of("LARGE"));
assertThat(current.getPlanJson()).contains("0.036").contains("LARGE");
assertThat(lot.getRemainingQuantity()).isEqualByComparingTo("6");
}
/** 未收到成功冻结回执时,原库存与计租份额都不能扣减。 */
@Test void pendingOrFailedFreezeCannotConsumeInventory() {
assertThat(service.prepare("9",request).ready()).isFalse();
@@ -0,0 +1,54 @@
package com.baiyue.lastleg.storagerent.service;
import com.baiyue.lastleg.storagerent.mapper.StorageRentBusinessSourceMapper;
import com.baiyue.lastleg.storagerent.repository.StorageRentRepository;
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 org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
/** 执行实际Mapper验证实收批次、客户、仓位、原托隔离及实箱离仓更新。 */
class StorageRentTransferVolumePersistenceTest {
/** 大小箱测量按原批次读取,其他仓位和客户不得混入,出库后不再返回原箱。 */
@Test void readsMeasuredCartonsFromExactReceiptAndUpdatesOnlyOwnedCarton() {
var ds=new DriverManagerDataSource("jdbc:h2:mem:rent-volume-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
var db=new JdbcTemplate(ds);
db.execute("CREATE TABLE ll_storage_rent_lot(id BIGINT,source_key VARCHAR(100),customer_id VARCHAR(30),warehouse_id VARCHAR(30),business_id BIGINT,business_type VARCHAR(40),rule_code VARCHAR(20))");
db.execute("CREATE TABLE ll_inbound_stock_receipt(id BIGINT,customer_id VARCHAR(30),warehouse_pallet_id BIGINT,sku_code VARCHAR(30))");
db.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT,customer_id VARCHAR(30),operating_warehouse_id VARCHAR(30))");
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(30),overseas_warehouse_id VARCHAR(30),pallet_id BIGINT)");
db.execute("CREATE TABLE ll_warehouse_transfer_pallet(id BIGINT,transfer_order_id BIGINT,customer_id VARCHAR(30),warehouse_pallet_id BIGINT,status VARCHAR(30))");
db.execute("CREATE TABLE ll_warehouse_transfer_item(id BIGINT,transfer_order_id BIGINT,customer_id VARCHAR(30),sku_code VARCHAR(30))");
db.execute("CREATE TABLE ll_warehouse_transfer_carton(id BIGINT,transfer_order_id BIGINT,customer_id VARCHAR(30),transfer_pallet_id BIGINT,transfer_item_id BIGINT,carton_mark VARCHAR(30),customer_carton_mark VARCHAR(30),status VARCHAR(30),length_cm DECIMAL(12,2),width_cm DECIMAL(12,2),height_cm DECIMAL(12,2),updated_at TIMESTAMP)");
db.update("INSERT INTO ll_storage_rent_lot VALUES(51,'INBOUND:101','A','WH',81,'WAREHOUSE_TRANSFER','TRANSIT')");
db.update("INSERT INTO ll_inbound_stock_receipt VALUES(101,'A',31,'SKU')");
db.update("INSERT INTO ll_warehouse_transfer_order VALUES(81,'A','WH')");
db.update("INSERT INTO ll_inventory_position VALUES(21,'A','WH',31),(22,'A','WH',32),(23,'B','WH',31)");
db.update("INSERT INTO ll_warehouse_transfer_pallet VALUES(71,81,'A',31,'COMPLETED'),(72,81,'A',32,'COMPLETED')");
db.update("INSERT INTO ll_warehouse_transfer_item VALUES(91,81,'A','SKU'),(92,81,'A','OTHER')");
db.update("INSERT INTO ll_warehouse_transfer_carton VALUES(1,81,'A',71,91,'SMALL',NULL,'COUNTED',30,20,20,NULL),(2,81,'A',71,91,'LARGE',NULL,'DISCARD_PENDING',40,30,30,NULL),(3,81,'B',71,91,'FOREIGN',NULL,'COUNTED',40,30,30,NULL),(4,81,'A',72,91,'OTHER-PALLET',NULL,'COUNTED',40,30,30,NULL),(5,81,'A',71,92,'OTHER-SKU',NULL,'COUNTED',40,30,30,NULL)");
var config=new Configuration(new Environment("test",new JdbcTransactionFactory(),ds));
config.setMapUnderscoreToCamelCase(true);config.addMapper(StorageRentBusinessSourceMapper.class);
try(var session=new SqlSessionFactoryBuilder().build(config).openSession()) {
var repository=new StorageRentRepository();ReflectionTestUtils.setField(repository,"businessSources",session.getMapper(StorageRentBusinessSourceMapper.class));
var boxes=repository.transferRentCartons("A","WH",21L,51L);
assertThat(boxes).extracting(b->b.cartonMark()).containsExactly("SMALL","LARGE");
assertThat(boxes.get(1).lengthCm()).isEqualByComparingTo("40");
assertThat(repository.transferRentCartons("B","WH",21L,51L)).isEmpty();
assertThat(repository.transferRentCartons("A","OTHER",21L,51L)).isEmpty();
assertThat(repository.transferRentCartons("A","WH",22L,51L)).isEmpty();
assertThat(repository.transferRentCartons("A","WH",23L,51L)).isEmpty();
assertThat(repository.shipTransferRentCartons("B",81L,List.of("SMALL"))).isZero();
assertThat(repository.shipTransferRentCartons("A",81L,List.of("SMALL","LARGE"))).isEqualTo(1);
assertThat(repository.transferRentCartons("A","WH",21L,51L)).extracting(b->b.cartonMark()).containsExactly("LARGE");
assertThat(repository.shipTransferRentCartons("A",81L,List.of("SMALL"))).isZero();
}
}
}
@@ -0,0 +1,122 @@
package com.baiyue.lastleg.storagerent.service;
import com.baiyue.lastleg.storagerent.dto.StorageRentDtos.*;
import com.baiyue.lastleg.storagerent.entity.*;
import com.baiyue.lastleg.storagerent.repository.StorageRentRepository;
import com.baiyue.lastleg.storagerent.gateway.StorageRentPricingGateway;
import com.baiyue.lastleg.discard.repository.CargoDiscardPdaRepository;
import com.baiyue.lastleg.discard.entity.*;
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos.ActualLine;
import com.baiyue.lastleg.dispatch.entity.*;
import com.baiyue.lastleg.delivery.entity.*;
import com.baiyue.lastleg.inventory.entity.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
/** 大小箱混托必须按实际出库箱体积计租,数量不再充当体积比例。 */
class StorageRentTransferVolumeTest {
private final StorageRentRepository repository=mock(StorageRentRepository.class);
private final CargoDiscardPdaRepository discards=mock(CargoDiscardPdaRepository.class);
private final StorageRentPricingGateway pricing=mock(StorageRentPricingGateway.class);
private final StorageRentSourceResolver resolver=new StorageRentSourceResolver();
private final LocalDateTime at=LocalDateTime.of(2026,11,4,7,37);
private final TransferRentCarton small=box(1L,"SMALL","COUNTED","30","20","20");
private final TransferRentCarton large=box(2L,"LARGE","COUNTED","40","30","30");
/** 原批次两箱共0.048立方米,库龄96天,模拟本次真实单据。 */
@BeforeEach void setup() {
ReflectionTestUtils.setField(resolver,"repository",repository);
ReflectionTestUtils.setField(resolver,"discards",discards);
ReflectionTestUtils.setField(resolver,"pricingGateway",pricing);
when(pricing.current("101")).thenReturn(StorageRentCalculator.defaults(101L));
var lot=new StorageRentLot();lot.setId(51L);lot.setBusinessType("WAREHOUSE_TRANSFER");lot.setRuleCode("TRANSIT");
lot.setOriginalQuantity(new BigDecimal("2"));lot.setRemainingQuantity(new BigDecimal("2"));
lot.setOriginalUnits(new BigDecimal("0.048"));lot.setInboundAt(at.minusDays(96));
var share=new StorageRentShare();share.setId(61L);share.setLotId(51L);share.setQuantity(new BigDecimal("2"));
var position=new InventoryPosition();position.setId(21L);position.setCustomerId("101");position.setOverseasWarehouseId("9");
position.setBalanceId(71L);position.setBatchNo("ZY-1");position.setPalletNo("PALLET");
var balance=new InventoryBalance();balance.setCustomerId("101");balance.setInboundOrderId(91L);
var transfer=new StorageRentBusinessSource();transfer.setId(81L);transfer.setBusinessNo("ZY-1");transfer.setInboundOrderId(91L);
var dispatch=new DispatchAggregate();dispatch.setId(1L);dispatch.setStatus("ACTIVE");dispatch.setStage("LOADING");dispatch.setOverseasWarehouseId("9");
var item=new DispatchItem();item.setDispatchOrderId(1L);item.setCustomerId("101");item.setOverseasWarehouseId("9");
item.setSourceBusinessType("WAREHOUSE_TRANSFER");item.setSourceBusinessNo("ZY-1");
when(repository.lockLot(51L)).thenReturn(lot);when(repository.stockShares("101","9",71L,"ZY-1")).thenReturn(List.of(share));
when(repository.position(21L)).thenReturn(position);when(repository.balance(71L)).thenReturn(balance);
when(repository.transfer("101","ZY-1")).thenReturn(transfer);
when(repository.dispatch("PC-1")).thenReturn(dispatch);when(repository.dispatchItem(11L)).thenReturn(item);
when(repository.transferRentCartons("101","9",21L,51L)).thenReturn(List.of(small,large));
}
/** 大箱0.036立方米应收3.79美元,小箱0.012立方米应收1.26美元。 */
@Test void dispatchUsesSelectedCartonVolumeAndKeepsSavedPricing() {
var plan=resolver.resolve("9",request(1,List.of("LARGE")),at);
assertThat(plan.amount()).isEqualByComparingTo("3.79");
assertThat(plan.calculations().get(0).billingQuantity()).isEqualByComparingTo("0.036");
assertThat(plan.allocations().get(0).quantity()).isEqualByComparingTo("1");
assertThat(plan.allocations().get(0).cartonMarks()).containsExactly("LARGE");
clearInvocations(pricing);
assertThat(resolver.resolve("9",request(1,List.of("LARGE")),at,plan.pricing()).amount()).isEqualByComparingTo("3.79");
verifyNoInteractions(pricing);
assertThat(resolver.resolve("9",request(1,List.of("SMALL")),at).amount()).isEqualByComparingTo("1.26");
}
/** 整托按所有实箱体积求和,未指定箱唛的部分出库必须阻断。 */
@Test void wholePalletSumsRealVolumesButPartialPickNeedsIdentity() {
var plan=resolver.resolve("9",request(2,List.of()),at);
assertThat(plan.amount()).isEqualByComparingTo("5.06");
assertThat(plan.calculations().get(0).billingQuantity()).isEqualByComparingTo("0.048");
assertThatThrownBy(()->resolver.resolve("9",request(1,List.of()),at)).hasMessageContaining("实际出库箱唛");
assertThatThrownBy(()->resolver.resolve("9",request(1,List.of("OTHER")),at)).hasMessageContaining("实际出库箱唛");
assertThatThrownBy(()->resolver.resolve("9",request(2,List.of("LARGE","LARGE")),at)).hasMessageContaining("重复");
}
/** 直接出库与排车使用同一真实计费体积。 */
@Test void outboundUsesActualVolume() {
var task=new OutboundPdaTask();task.setCustomerId("101");task.setOutboundOrderId(41L);task.setStatus("PENDING");
var outbound=new OutboundOrder();outbound.setOverseasWarehouseId("9");outbound.setOutboundType("WAREHOUSE_TRANSFER_OUTBOUND");
var item=new OutboundPalletItem();item.setCustomerId("101");item.setOutboundOrderId(41L);item.setOrderRefs("ZY-1");
when(repository.task("9","TASK")).thenReturn(task);when(repository.outbound(41L)).thenReturn(outbound);when(repository.outboundItem(11L)).thenReturn(item);
var request=new PrepareRequest("OUTBOUND","TASK",11L,21L,1L,null,null,"scan",List.of("LARGE"),null,null);
assertThat(resolver.resolve("9",request,at).amount()).isEqualByComparingTo("3.79");
}
/** 废弃选中的大箱仍按0.036立方米计费,预留状态不使它被平均分摊。 */
@Test void discardBillsOnlyTheReservedActualCarton() {
var application=new CargoDiscardApplication();application.setId(101L);application.setCustomerId("101");application.setDiscardNo("CD-1");application.setStatus("APPROVED");
var item=new CargoDiscardItem();item.setId(201L);item.setApplicationId(101L);item.setCustomerId("101");item.setDiscardPackages(1);item.setInventoryPositionId(21L);
var link=new CargoDiscardCarton();link.setItemId(201L);link.setCartonMark("LARGE");
when(discards.lockApplication("CD-1","9")).thenReturn(application);when(discards.item(201L)).thenReturn(item);when(discards.cartons("101",101L)).thenReturn(List.of(link));
when(repository.transferRentCartons("101","9",21L,51L)).thenReturn(List.of(small,box(2L,"LARGE","DISCARD_PENDING","40","30","30")));
var request=new PrepareRequest("DISCARD","CD-1",null,null,null,null,"PALLET","scan",null,null,null,
List.of(new ActualLine(201L,1)),List.of(),List.of("LARGE"),false);
var plan=resolver.resolve("9",request,at);
assertThat(plan.amount()).isEqualByComparingTo("3.79");
assertThat(plan.calculations().get(0).billingQuantity()).isEqualByComparingTo("0.036");
assertThat(plan.owner().businessNo()).isEqualTo("CD-1");
}
/** 缺测量、错误来源或已被废弃预留的箱不允许降级成平均体积。 */
@Test void missingMeasurementsAndUnavailableCartonsBlockCharging() {
when(repository.transferRentCartons("101","9",21L,51L)).thenReturn(List.of(box(2L,"LARGE","COUNTED","0","30","30")));
assertThatThrownBy(()->resolver.resolve("9",request(1,List.of("LARGE")),at)).hasMessageContaining("实际长宽高");
when(repository.transferRentCartons("101","9",21L,51L)).thenReturn(List.of());
assertThatThrownBy(()->resolver.resolve("9",request(1,List.of("LARGE")),at)).hasMessageContaining("实际出库箱唛");
when(repository.transferRentCartons("101","9",21L,51L)).thenReturn(List.of(box(2L,"LARGE","DISCARD_PENDING","40","30","30")));
assertThatThrownBy(()->resolver.resolve("9",request(1,List.of("LARGE")),at)).hasMessageContaining("实际出库箱唛");
}
/** 创建真实排车扫码请求,费用客户和仓库仅由持久化单据给出。 */
private PrepareRequest request(long packages,List<String> marks) {
return new PrepareRequest("DISPATCH","PC-1",11L,21L,packages,null,null,"scan",marks,null,null);
}
/** 尺寸保持厘米原精度,不预先将单箱体积舍入到三位。 */
private TransferRentCarton box(Long id,String mark,String status,String length,String width,String height) {
return new TransferRentCarton(id,mark,null,status,new BigDecimal(length),new BigDecimal(width),new BigDecimal(height));
}
}