修复客户尺寸加权与入库计费验收

This commit is contained in:
2026-08-08 09:43:56 +08:00
parent f3c2bafec8
commit 2779bf9ff7
17 changed files with 967 additions and 58 deletions
@@ -129,7 +129,7 @@ class CustomerDimensionConfigSqlContractTest {
.contains("现有企业客户未完整持有固定四箱规尺寸配置");
String repeatSafeClause = sql.substring(backfill,
sql.indexOf("DROP PROCEDURE IF EXISTS unified_assert_final_extensions", backfill));
sql.indexOf("-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql", backfill));
assertThat(repeatSafeClause)
.doesNotContain("edge_addition_cm = VALUES")
.doesNotContain("inner_length_cm = VALUES")
@@ -90,11 +90,15 @@ class InboundParentChildSqlContractTest {
@Test
void syncsUnifiedMigration() throws Exception {
String module = normalize(sql).trim();
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
assertThat(unified)
.contains("-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql")
.contains(module);
.contains("CREATE PROCEDURE phase3_inbound_add_column")
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_batch")
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_item")
.contains("CREATE TRIGGER trg_inbound_order_parent_before_insert")
.contains("CREATE PROCEDURE phase3_assert_inbound_parent_child_schema")
.contains("旧订单唯一索引仍阻止创建子入仓单");
}
private static String loadSql() {
@@ -5,10 +5,13 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -39,17 +42,22 @@ class CartonMeasurementServiceTest {
private final CartonLabelBindingResolver resolver = mock(CartonLabelBindingResolver.class);
private final MeasurementImageClient images = mock(MeasurementImageClient.class);
private final TransactionTemplate transactions = mock(TransactionTemplate.class);
private final CustomerDimensionConfigRepository dimensionConfigs = mock(CustomerDimensionConfigRepository.class);
private final CustomerDimensionWeightingService dimensionWeighting =
new CustomerDimensionWeightingService(dimensionConfigs);
private CartonMeasurementService service;
private final AtomicReference<InboundMeasurementItemEntity> insertedItem = new AtomicReference<>();
@BeforeEach
void setUp() {
service = new CartonMeasurementService(repository, resolverProvider, images, transactions);
service = new CartonMeasurementService(repository, resolverProvider, images, transactions,
dimensionWeighting);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
when(resolverProvider.getIfAvailable()).thenReturn(resolver);
when(resolver.resolveByBoxCode(10001L, "RK-001#5-1"))
.thenReturn(new CartonLabelBindingResolver.Binding(70001L, 80001L, "RK-001", 1, 5, "50x40x30cm"));
when(repository.findInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
when(dimensionConfigs.findAll(10001L)).thenReturn(dimensionRules(10001L));
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
when(images.upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()))
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
@@ -79,7 +87,10 @@ class CartonMeasurementServiceTest {
assertThat(result.confirmedPackageCount()).isEqualTo(1);
assertThat(result.boxCode()).isEqualTo("RK-001#5-1");
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.0733");
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1870");
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.000");
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("54.000");
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("54.000");
}
@Test
@@ -96,7 +107,7 @@ class CartonMeasurementServiceTest {
assertThat(result.created()).isFalse();
assertThat(result.confirmedPackageCount()).isEqualTo(7);
assertThat(existing.getLengthCm()).isEqualByComparingTo("60.100");
assertThat(existing.getLengthCm()).isEqualByComparingTo("59.100");
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
}
@@ -139,6 +150,29 @@ class CartonMeasurementServiceTest {
verify(repository).updateMeasurement(measurement);
}
@Test
void confirmedPackageCountPreservesTheRuleSnapshotAfterCustomerConfigChanges() {
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 2);
InboundMeasurementItemEntity item = item(40001L, 30001L, 1, 99001L);
item.setOuterLengthCm(new BigDecimal("34"));
item.setOuterWidthCm(new BigDecimal("24"));
item.setOuterHeightCm(new BigDecimal("24"));
item.setUnitVolumeCbm(new BigDecimal("0.020"));
item.setCalculationRuleVersion("CUSTOMER_DIMENSION_SMALL_V3");
when(repository.findItem(10001L, 40001L)).thenReturn(item);
when(repository.findMeasurement(10001L, 70001L, 30001L)).thenReturn(measurement);
when(repository.updateItem(item)).thenReturn(true);
when(repository.listItems(10001L, 30001L)).thenReturn(List.of(item));
when(repository.updateMeasurement(measurement)).thenReturn(true);
service.updateConfirmedPackageCount(10001L, 70001L, 40001L, 2, 3);
assertThat(item.getTotalVolumeCbm()).isEqualByComparingTo("0.0600");
assertThat(item.getOuterLengthCm()).isEqualByComparingTo("34");
assertThat(item.getCalculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_SMALL_V3");
verify(dimensionConfigs, never()).findAll(10001L);
}
@Test
void confirmedPackageCountCanBeCorrectedAfterSubmissionWithoutChangingOriginalPackageCount() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
@@ -158,13 +192,13 @@ class CartonMeasurementServiceTest {
service.updateConfirmedPackageCount(10001L, 70001L, 40001L, 2, 5);
assertThat(inbound.getTotalPackageCount()).isEqualTo(5);
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("0.3000");
assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("50.100");
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("0.5250");
assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("87.675");
verify(repository).updateInbound(inbound);
}
private CartonMeasurementCommand command() {
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("60.100"),
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("59.100"),
new BigDecimal("40.200"), new BigDecimal("30.300"), new BigDecimal("12.500"),
"MJ-01", LocalDateTime.parse("2026-08-07T10:30:00"),
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
@@ -193,4 +227,15 @@ class CartonMeasurementServiceTest {
item.setSourceImageFileId(imageId); item.setItemNo("B1"); item.setBoxSpecification("50x40x30cm");
return item;
}
private List<CustomerDimensionConfigEntity> dimensionRules(long tenantId) {
return java.util.Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(0);
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
rule.setInnerHeightCm(type.defaultHeightCm());
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
return rule;
}).toList();
}
}
@@ -0,0 +1,64 @@
package com.baiyue.firstleg.service;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.pricing.FbaWarehouseRateEstimator;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** Representative test-environment data flowing from dimension weighting into channel pricing. */
class CustomerDimensionPricingFlowTest {
private final CustomerDimensionConfigRepository configs = mock(CustomerDimensionConfigRepository.class);
private final CustomerDimensionWeightingService dimensions = new CustomerDimensionWeightingService(configs);
private final FbaWarehouseRateEstimator pricing = new FbaWarehouseRateEstimator();
@Test
void channelRateChoosesWeightOrVolumeAfterCustomerDimensionWeighting() {
when(configs.findAll(10001L)).thenReturn(defaultRules());
var small = dimensions.load(10001L).resolve(
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
BigDecimal twoCartonsVolume = small.unitVolumeCbm().multiply(new BigDecimal("2"));
BigDecimal actualWeight = new BigDecimal("24");
var kgEstimate = pricing.estimate(rate("KG", "8.700000"), actualWeight, twoCartonsVolume);
var cbmEstimate = pricing.estimate(rate("CBM", "1400.000000"), actualWeight, twoCartonsVolume);
assertThat(twoCartonsVolume).isEqualByComparingTo("0.040");
assertThat(kgEstimate.quantity()).isEqualByComparingTo("24.000000");
assertThat(kgEstimate.amount()).isEqualByComparingTo("208.80");
assertThat(cbmEstimate.quantity()).isEqualByComparingTo("0.066116");
assertThat(cbmEstimate.amount()).isEqualByComparingTo("92.56");
}
private RateRow rate(String unit, String price) {
RateRow rate = new RateRow();
rate.setChannelCode("EXX_TRUCK");
rate.setDestinationType("OVERSEAS");
rate.setDestinationWarehouseId(193000000000200003L);
rate.setDestinationName("洛杉矶仓");
rate.setChargeUnit(unit);
rate.setUnitPrice(new BigDecimal(price));
rate.setCurrency("CNY");
return rate;
}
private List<CustomerDimensionConfigEntity> defaultRules() {
return Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
rule.setTenantId(10001L); rule.setBoxType(type); rule.setVersion(0);
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
rule.setInnerHeightCm(type.defaultHeightCm());
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
return rule;
}).toList();
}
}
@@ -0,0 +1,85 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class CustomerDimensionWeightingServiceTest {
private final CustomerDimensionConfigRepository repository = mock(CustomerDimensionConfigRepository.class);
private final CustomerDimensionWeightingService service = new CustomerDimensionWeightingService(repository);
@BeforeEach
void setUp() {
when(repository.findAll(10001L)).thenReturn(defaultRules(10001L));
}
@Test
void measuredDimensionsUseSmallestContainingRuleAndAddEachEdgeTwice() {
var weighted = service.load(10001L).resolve(
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("34.000");
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("24.000");
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("24.000");
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.019584");
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.020");
}
@Test
void cartonRotationDoesNotChangeTheMatchedRule() {
var weighted = service.load(10001L).resolve(
new BigDecimal("20"), new BigDecimal("30"), new BigDecimal("20"));
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
}
@Test
void crossingOneSmallBoxBoundaryUsesTheNextContainingRule() {
var weighted = service.load(10001L).resolve(
new BigDecimal("30.001"), new BigDecimal("20"), new BigDecimal("20"));
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.MEDIUM);
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.051");
}
@Test
void dimensionsBeyondTheLargestRuleAreRejected() {
assertThatThrownBy(() -> service.load(10001L).resolve(
new BigDecimal("60.001"), new BigDecimal("50"), new BigDecimal("50")))
.isInstanceOf(BizException.class)
.hasMessageContaining("超出客户最大箱规");
}
@Test
void incompleteCustomerRulesAreRejectedBeforeMeasurementCalculation() {
when(repository.findAll(10001L)).thenReturn(defaultRules(10001L).subList(0, 3));
assertThatThrownBy(() -> service.load(10001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("配置不完整");
}
private List<CustomerDimensionConfigEntity> defaultRules(long tenantId) {
return Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(3);
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
rule.setInnerHeightCm(type.defaultHeightCm());
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
return rule;
}).toList();
}
}
@@ -114,8 +114,6 @@ class FbaPricingServiceTest {
readyRepository();
InboundPricingEntity inbound = inbound("100.000", "1.0000");
inbound.setProcessStatus("INBOUNDED");
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
estimate("KG", "167.000", "2.000000", "334.00", "0.00"));
@@ -3,7 +3,6 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
@@ -519,7 +518,7 @@ class InboundMeasurementServiceTest {
saved.setVersion(0);
saved.setStatus("DRAFT");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(orderCargoClient.detail(eq(""), eq(1L), eq(80L), eq(80001L))).thenReturn(
when(orderCargoClient.detail(eq("internal-token"), eq(1L), eq(80L), eq(80001L))).thenReturn(
ApiResponse.success(new OrderCargoClient.CargoBinding(80001L, 80L, 1, "Test cargo",
2, new BigDecimal("6.500"), new BigDecimal("0.0480"))));
when(batchService.saveBatch(any())).thenReturn(saved);
@@ -534,7 +533,7 @@ class InboundMeasurementServiceTest {
ArgumentCaptor<MeasurementBatchCommand> command = ArgumentCaptor.forClass(MeasurementBatchCommand.class);
verify(batchService).saveBatch(command.capture());
assertThat(command.getValue().items().get(0).inboundDetailId()).isEqualTo(80001L);
verify(orderCargoClient).detail("", 1L, 80L, 80001L);
verify(orderCargoClient).detail("internal-token", 1L, 80L, 80001L);
}
private InboundOrderEntity inbound(String status) {
@@ -2,10 +2,13 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -27,14 +30,16 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("测量批次共享领域服务")
@SpringJUnitConfig(MeasurementBatchService.class)
@SpringJUnitConfig({MeasurementBatchService.class, CustomerDimensionWeightingService.class})
class MeasurementBatchServiceTest {
@MockBean private InboundMeasurementRepository repository;
@MockBean private CustomerDimensionConfigRepository dimensionConfigs;
@Resource private MeasurementBatchService service;
@BeforeEach
void setUp() {
when(repository.findInbound(1L, 10L)).thenReturn(inbound());
when(dimensionConfigs.findAll(1L)).thenReturn(dimensionRules(1L));
org.mockito.Mockito.doAnswer(invocation -> {
InboundMeasurementEntity entity = invocation.getArgument(0);
entity.setId(100L);
@@ -57,8 +62,13 @@ class MeasurementBatchServiceTest {
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(100L), items.capture());
assertThat(items.getValue().get(0).getUnitVolumeCbm()).isEqualByComparingTo("0.0240");
assertThat(items.getValue().get(0).getTotalVolumeCbm()).isEqualByComparingTo("0.0480");
assertThat(items.getValue().get(0).getUnitVolumeCbm()).isEqualByComparingTo("0.0510");
assertThat(items.getValue().get(0).getTotalVolumeCbm()).isEqualByComparingTo("0.1020");
assertThat(items.getValue().get(0).getOuterLengthCm()).isEqualByComparingTo("44.000");
assertThat(items.getValue().get(0).getOuterWidthCm()).isEqualByComparingTo("34.000");
assertThat(items.getValue().get(0).getOuterHeightCm()).isEqualByComparingTo("34.000");
assertThat(items.getValue().get(0).getCalculationRuleVersion())
.isEqualTo("CUSTOMER_DIMENSION_MEDIUM_V0");
assertThat(items.getValue().get(0).getGirthCm()).isEqualByComparingTo("140.000");
assertThat(items.getValue().get(0).getUnitWeightKg()).isNull();
verify(repository, never()).updateInbound(any());
@@ -74,8 +84,8 @@ class MeasurementBatchServiceTest {
InboundMeasurementEntity saved = service.saveBatch(command);
assertThat(saved.getStatus()).isEqualTo("DRAFT");
assertThat(saved.getTotalVolumeCbm()).isEqualByComparingTo("0.0480");
assertThat(saved.getVolumeWeightKg()).isEqualByComparingTo("8.016");
assertThat(saved.getTotalVolumeCbm()).isEqualByComparingTo("0.1020");
assertThat(saved.getVolumeWeightKg()).isEqualByComparingTo("17.034");
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(100L), items.capture());
@@ -145,4 +155,15 @@ class MeasurementBatchServiceTest {
inbound.setVersion(0);
return inbound;
}
private List<CustomerDimensionConfigEntity> dimensionRules(long tenantId) {
return java.util.Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(0);
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
rule.setInnerHeightCm(type.defaultHeightCm());
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
return rule;
}).toList();
}
}