提交
This commit is contained in:
+25
@@ -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();
|
||||
|
||||
+54
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user