From 1a1a19dea7c0ec715ee2946bdc9475102a4cdee3 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Tue, 8 Sep 2026 18:04:14 +0800
Subject: [PATCH] feat(firstleg): tier container customer freight
---
.../mapper/ContainerTierPricingMapper.java | 46 +++++++++
.../pricing/ContainerTierAmountAllocator.java | 72 ++++++++++++++
.../repository/FbaPricingRepository.java | 7 ++
.../firstleg/service/FbaPricingService.java | 48 ++++++++-
.../ContainerTierAmountAllocatorTest.java | 61 ++++++++++++
.../service/FbaPricingServiceTest.java | 98 +++++++++++++++++++
6 files changed, 331 insertions(+), 1 deletion(-)
create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerTierPricingMapper.java
create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocator.java
create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocatorTest.java
diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerTierPricingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerTierPricingMapper.java
new file mode 100644
index 000000000..70553bdce
--- /dev/null
+++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerTierPricingMapper.java
@@ -0,0 +1,46 @@
+package com.baiyue.firstleg.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/** 同柜同客户区域重量阶梯计费查询。 */
+@Mapper
+public interface ContainerTierPricingMapper {
+
+ @Select("""
+
+ """)
+ List members(@Param("customerId") long customerId,
+ @Param("inboundId") long inboundId);
+
+ record MemberRow(long containerBookingId, long inboundId, BigDecimal actualWeightKg,
+ BigDecimal actualVolumeCbm) {}
+}
diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocator.java b/firstleg/src/main/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocator.java
new file mode 100644
index 000000000..e8a23645c
--- /dev/null
+++ b/firstleg/src/main/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocator.java
@@ -0,0 +1,72 @@
+package com.baiyue.firstleg.pricing;
+
+import com.baiyue.common.core.api.ResultCode;
+import com.baiyue.common.core.exception.BizException;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** 将同柜同客户的阶梯总额按各票计费重分摊,并保证分摊总额守恒。 */
+@Component
+public class ContainerTierAmountAllocator {
+ private static final int MONEY_SCALE = 2;
+ private static final BigDecimal CENT = new BigDecimal("0.01");
+
+ public Allocation allocate(List source, BigDecimal unitPrice) {
+ if (source == null || source.isEmpty() || unitPrice == null || unitPrice.signum() < 0) {
+ throw validation("柜级阶梯计费参数无效");
+ }
+ List members = source.stream()
+ .sorted(Comparator.comparingLong(Member::inboundId))
+ .toList();
+ if (members.stream().map(Member::inboundId).distinct().count() != members.size()) {
+ throw validation("柜级阶梯计费成员不能重复");
+ }
+ BigDecimal totalQuantity = members.stream().map(Member::chargeableQuantity)
+ .peek(value -> {
+ if (value == null || value.signum() < 0) throw validation("计费重不能小于0");
+ })
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ if (totalQuantity.signum() <= 0) throw validation("柜级合计计费重必须大于0");
+
+ BigDecimal totalAmount = totalQuantity.multiply(unitPrice).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
+ List shares = new ArrayList<>(members.size());
+ BigDecimal allocated = BigDecimal.ZERO.setScale(MONEY_SCALE);
+ for (Member member : members) {
+ BigDecimal exact = totalAmount.multiply(member.chargeableQuantity())
+ .divide(totalQuantity, 12, RoundingMode.HALF_UP);
+ BigDecimal floor = exact.setScale(MONEY_SCALE, RoundingMode.DOWN);
+ allocated = allocated.add(floor);
+ shares.add(new Share(member.inboundId(), floor, exact.subtract(floor)));
+ }
+ int cents = totalAmount.subtract(allocated).movePointRight(MONEY_SCALE).intValueExact();
+ shares.sort(Comparator.comparing(Share::remainder).reversed().thenComparingLong(Share::inboundId));
+ for (int index = 0; index < cents; index++) {
+ Share share = shares.get(index % shares.size());
+ shares.set(index % shares.size(), share.add(CENT));
+ }
+ shares.sort(Comparator.comparingLong(Share::inboundId));
+ Map amounts = new LinkedHashMap<>();
+ shares.forEach(share -> amounts.put(share.inboundId(), share.amount()));
+ return new Allocation(totalQuantity, totalAmount, amounts);
+ }
+
+ private static BizException validation(String message) {
+ return new BizException(ResultCode.VALIDATION_FAILED, message);
+ }
+
+ public record Member(long inboundId, BigDecimal chargeableQuantity) {}
+ public record Allocation(BigDecimal totalQuantity, BigDecimal totalAmount,
+ Map memberAmounts) {}
+ private record Share(long inboundId, BigDecimal amount, BigDecimal remainder) {
+ private Share add(BigDecimal increment) {
+ return new Share(inboundId, amount.add(increment), remainder);
+ }
+ }
+}
diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java
index 867e31cde..ae148006f 100644
--- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java
+++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java
@@ -18,6 +18,7 @@ import com.baiyue.firstleg.mapper.InboundFeeMapper;
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
import com.baiyue.firstleg.mapper.InboundPricingIdempotencyMapper;
import com.baiyue.firstleg.mapper.InboundPricingMapper;
+import com.baiyue.firstleg.mapper.ContainerTierPricingMapper;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
@@ -40,6 +41,12 @@ public class FbaPricingRepository {
private InboundPricingIdempotencyMapper idempotencies;
@Resource
private InboundOutboxMapper outbox;
+ @Resource
+ private ContainerTierPricingMapper containerTierPricing;
+
+ public List containerTierMembers(long customerId, long inboundId) {
+ return containerTierPricing.members(customerId, inboundId);
+ }
public void insertRule(CustomerChargeRuleEntity rule) { rules.insert(rule); }
diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java
index 501fd42d8..8ad0ebfea 100644
--- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java
+++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java
@@ -14,6 +14,8 @@ import com.baiyue.firstleg.entity.InboundFeeItemEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
+import com.baiyue.firstleg.pricing.ContainerTierAmountAllocator;
+import com.baiyue.firstleg.domain.PriceModel;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import org.springframework.stereotype.Service;
@@ -30,6 +32,8 @@ import java.time.Clock;
import java.time.LocalDateTime;
import java.util.HexFormat;
import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
/** FBA 费用试算、确认和发送状态机。 */
@Service
@@ -51,6 +55,8 @@ public class FbaPricingService {
private InboundFeeDetailService feeDetails;
@Resource
private FinanceFeeSourceOutboxService financeOutbox;
+ @Resource
+ private ContainerTierAmountAllocator containerTierAllocator;
@Transactional
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
@@ -123,9 +129,10 @@ public class FbaPricingService {
pricingTaxMode(inbound), policy.chargeMode(),
selectedWeightKg == null ? inbound.getActualNetWeightKg() : selectedWeightKg,
selectedVolumeCbm == null ? inbound.getActualVolumeCbm() : selectedVolumeCbm));
- RateRow rate = estimate.rate();
BigDecimal weight = (selectedWeightKg == null ? inbound.getActualNetWeightKg() : selectedWeightKg).setScale(3, RoundingMode.HALF_UP);
BigDecimal volume = (selectedVolumeCbm == null ? inbound.getActualVolumeCbm() : selectedVolumeCbm).setScale(4, RoundingMode.HALF_UP);
+ estimate = applyContainerCustomerTier(inbound, estimate, weight, volume);
+ RateRow rate = estimate.rate();
BigDecimal chargeable = estimate.chargeableQuantity();
BigDecimal converted = chargeable;
BigDecimal subtotal = estimate.amount().setScale(MONEY_SCALE, RoundingMode.HALF_UP);
@@ -167,6 +174,45 @@ public class FbaPricingService {
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
}
+ private ActivePriceBookEstimator.Estimate applyContainerCustomerTier(
+ InboundPricingEntity inbound, ActivePriceBookEstimator.Estimate individual,
+ BigDecimal currentWeight, BigDecimal currentVolume) {
+ if (individual.priceModel() != PriceModel.REGION_WEIGHT_TIER) return individual;
+ var rows = repository.containerTierMembers(inbound.getTenantId(), inbound.getId());
+ if (rows == null || rows.isEmpty()) return individual;
+ long containerId = rows.get(0).containerBookingId();
+ if (rows.stream().anyMatch(row -> row.containerBookingId() != containerId)) {
+ throw conflict("同一入仓单已分配到多个柜子,不能生成柜级阶梯费用");
+ }
+ Map unique = new LinkedHashMap<>();
+ for (var row : rows) {
+ BigDecimal weight = row.inboundId() == inbound.getId() ? currentWeight : row.actualWeightKg();
+ BigDecimal volume = row.inboundId() == inbound.getId() ? currentVolume : row.actualVolumeCbm();
+ BigDecimal quantity = chargeableWeight(weight, volume);
+ unique.putIfAbsent(row.inboundId(), new ContainerTierAmountAllocator.Member(row.inboundId(), quantity));
+ }
+ if (!unique.containsKey(inbound.getId()) || unique.size() < 2) return individual;
+ BigDecimal aggregate = unique.values().stream().map(ContainerTierAmountAllocator.Member::chargeableQuantity)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ ActivePriceBookEstimator.Estimate group = priceBookEstimator.estimate(inbound.getTenantId(), new EstimateRequest(
+ inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
+ destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
+ pricingTaxMode(inbound), "KG", aggregate, BigDecimal.ZERO));
+ var allocation = containerTierAllocator.allocate(List.copyOf(unique.values()), group.unitPrice());
+ BigDecimal memberQuantity = unique.get(inbound.getId()).chargeableQuantity();
+ return new ActivePriceBookEstimator.Estimate(group.priceBookVersionId(), group.channelCode(),
+ group.priceModel(), group.pricingTaxMode(), group.rate(), memberQuantity, group.chargeUnit(),
+ group.unitPrice(), allocation.memberAmounts().get(inbound.getId()), group.currency(),
+ group.conversionFactor(), group.roundingScale(), group.roundingMode());
+ }
+
+ private static BigDecimal chargeableWeight(BigDecimal weight, BigDecimal volume) {
+ BigDecimal safeWeight = weight == null ? BigDecimal.ZERO : weight;
+ BigDecimal safeVolume = volume == null ? BigDecimal.ZERO : volume;
+ return safeWeight.max(safeVolume.multiply(BigDecimal.valueOf(167)))
+ .setScale(6, RoundingMode.HALF_UP);
+ }
+
@Transactional
public FeeSnapshotResponse confirm(long tenantId, long operatorId, long inboundId,
String idempotencyKey) {
diff --git a/firstleg/src/test/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocatorTest.java
new file mode 100644
index 000000000..9a4d8e0f8
--- /dev/null
+++ b/firstleg/src/test/java/com/baiyue/firstleg/pricing/ContainerTierAmountAllocatorTest.java
@@ -0,0 +1,61 @@
+package com.baiyue.firstleg.pricing;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ContainerTierAmountAllocatorTest {
+ private final ContainerTierAmountAllocator allocator = new ContainerTierAmountAllocator();
+
+ @Test
+ void allocatesOneGroupByChargeableWeightAndConservesRoundedTotal() {
+ var result = allocator.allocate(List.of(
+ member(31, "100"), member(11, "101"), member(21, "102")
+ ), new BigDecimal("9.99"));
+
+ assertThat(result.totalQuantity()).isEqualByComparingTo("303");
+ assertThat(result.totalAmount()).isEqualByComparingTo("3026.97");
+ assertThat(result.memberAmounts()).containsExactly(
+ java.util.Map.entry(11L, new BigDecimal("1008.99")),
+ java.util.Map.entry(21L, new BigDecimal("1018.98")),
+ java.util.Map.entry(31L, new BigDecimal("999.00")));
+ assertThat(result.memberAmounts().values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
+ .isEqualByComparingTo(result.totalAmount());
+ }
+
+ @Test
+ void distributesRoundingCentsDeterministically() {
+ var result = allocator.allocate(List.of(member(3, "1"), member(2, "1"), member(1, "1")),
+ new BigDecimal("0.005"));
+
+ assertThat(result.memberAmounts()).containsExactly(
+ java.util.Map.entry(1L, new BigDecimal("0.01")),
+ java.util.Map.entry(2L, new BigDecimal("0.01")),
+ java.util.Map.entry(3L, new BigDecimal("0.00")));
+ assertThat(result.memberAmounts().values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
+ .isEqualByComparingTo("0.02");
+ }
+
+ @Test
+ void keepsSingleInboundAmountAndQuantity() {
+ var result = allocator.allocate(List.of(member(11, "1.337")), new BigDecimal("10"));
+
+ assertThat(result.totalQuantity()).isEqualByComparingTo("1.337");
+ assertThat(result.totalAmount()).isEqualByComparingTo("13.37");
+ assertThat(result.memberAmounts()).containsExactly(java.util.Map.entry(11L, new BigDecimal("13.37")));
+ }
+
+ @Test
+ void rejectsDuplicateInboundMembers() {
+ assertThatThrownBy(() -> allocator.allocate(List.of(member(11, "1"), member(11, "2")), BigDecimal.ONE))
+ .hasMessageContaining("不能重复");
+ }
+
+ private static ContainerTierAmountAllocator.Member member(long inboundId, String weight) {
+ return new ContainerTierAmountAllocator.Member(inboundId, new BigDecimal(weight));
+ }
+}
diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java
index 5b206acf2..c5e7d40c5 100644
--- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java
+++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java
@@ -12,13 +12,16 @@ import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
+import com.baiyue.firstleg.mapper.ContainerTierPricingMapper;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
+import com.baiyue.firstleg.pricing.ContainerTierAmountAllocator;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import org.apache.ibatis.annotations.Select;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -31,6 +34,7 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.atomic.AtomicReference;
import java.util.List;
+import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -58,6 +62,7 @@ class FbaPricingServiceTest {
@MockBean private com.baiyue.firstleg.repository.InboundMeasurementRepository measurementRepository;
@MockBean private InboundFeeDetailService feeDetails;
@MockBean private ActivePriceBookEstimator priceBookEstimator;
+ @MockBean private ContainerTierAmountAllocator containerTierAllocator;
@Resource private FbaPricingService service;
@Test
@@ -677,6 +682,80 @@ class FbaPricingServiceTest {
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
}
+ @Test
+ @DisplayName("同客户同柜区域阶梯按总计费重命中临界档并按票分摊")
+ void appliesContainerCustomerTierAndAllocatedAmount() {
+ readyRepository();
+ InboundPricingEntity inbound = inbound("100", "0.1");
+ when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
+ when(repository.containerTierMembers(TENANT, INBOUND)).thenReturn(List.of(
+ memberRow(901L, INBOUND, "100", "0.1"),
+ memberRow(901L, 30002L, "100", "0.1")
+ ));
+ when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(
+ regionEstimate("100", "10", "1000"), regionEstimate("200", "8", "1600"));
+ when(containerTierAllocator.allocate(any(), eq(new BigDecimal("8")))).thenReturn(
+ new ContainerTierAmountAllocator.Allocation(new BigDecimal("200"), new BigDecimal("1600"),
+ Map.of(INBOUND, new BigDecimal("800.00"), 30002L, new BigDecimal("800.00"))));
+
+ FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "container-tier");
+
+ assertThat(response.chargeableQuantity()).isEqualByComparingTo("100");
+ assertThat(response.unitPrice()).isEqualByComparingTo("8");
+ assertThat(response.subtotalAmount()).isEqualByComparingTo("800.00");
+ assertThat(response.totalAmount()).isEqualByComparingTo("800.00");
+ ArgumentCaptor requests =
+ ArgumentCaptor.forClass(com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest.class);
+ verify(priceBookEstimator, times(2)).estimate(eq(TENANT), requests.capture());
+ assertThat(requests.getAllValues().get(1).actualWeightKg()).isEqualByComparingTo("200");
+ assertThat(requests.getAllValues().get(1).actualVolumeCbm()).isEqualByComparingTo(BigDecimal.ZERO);
+ }
+
+ @Test
+ @DisplayName("单票柜次保持原区域阶梯结果,不进行无意义分摊")
+ void keepsIndividualResultForSingleInboundContainer() {
+ readyRepository();
+ InboundPricingEntity inbound = inbound("100", "0.1");
+ when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
+ when(repository.containerTierMembers(TENANT, INBOUND)).thenReturn(List.of(
+ memberRow(901L, INBOUND, "100", "0.1")
+ ));
+ when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(regionEstimate("100", "10", "1000"));
+
+ FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "single-container-tier");
+
+ assertThat(response.totalAmount()).isEqualByComparingTo("1000");
+ verify(containerTierAllocator, never()).allocate(any(), any());
+ verify(priceBookEstimator).estimate(eq(TENANT), any());
+ }
+
+ @Test
+ @DisplayName("无柜次关系不改变单票区域阶梯计价")
+ void keepsIndividualResultWithoutContainerRelation() {
+ readyRepository();
+ InboundPricingEntity inbound = inbound("100", "0.1");
+ when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
+ when(repository.containerTierMembers(TENANT, INBOUND)).thenReturn(List.of());
+ when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(regionEstimate("100", "10", "1000"));
+
+ assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "no-container-tier").totalAmount())
+ .isEqualByComparingTo("1000");
+ verify(containerTierAllocator, never()).allocate(any(), any());
+ }
+
+ @Test
+ @DisplayName("柜次成员查询只按客户和同一柜次隔离")
+ void scopesContainerTierMembersByCustomerAndContainer() throws NoSuchMethodException {
+ String sql = ContainerTierPricingMapper.class.getMethod("members", long.class, long.class)
+ .getAnnotation(Select.class).value()[0];
+
+ assertThat(sql).contains("plan.container_booking_id=current_plan.container_booking_id")
+ .contains("grouped_item.account_id=#{customerId}")
+ .contains("grouped_inbound.tenant_id=#{customerId}")
+ .doesNotContain("grouped_inbound.destination_code")
+ .doesNotContain("grouped_inbound.destination_warehouse_id");
+ }
+
private void readyRepository() {
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
@@ -725,6 +804,25 @@ class FbaPricingServiceTest {
"KG".equals(unit) ? new BigDecimal("167") : new BigDecimal("363"), 6, "HALF_UP");
}
+ private ContainerTierPricingMapper.MemberRow memberRow(long containerId, long inboundId,
+ String weight, String volume) {
+ return new ContainerTierPricingMapper.MemberRow(containerId, inboundId,
+ new BigDecimal(weight), new BigDecimal(volume));
+ }
+
+ private ActivePriceBookEstimator.Estimate regionEstimate(String quantity, String unitPrice, String amount) {
+ RateRow rate = new RateRow();
+ rate.setId(202L); rate.setPriceBookVersionId(701L); rate.setRateCode("PB-TIER-1"); rate.setRateVersion(1);
+ rate.setOriginWarehouseId(21L); rate.setOriginWarehouseName("义乌仓"); rate.setChannelCode("MATSON_TRUCK");
+ rate.setPriceModel(PriceModel.REGION_WEIGHT_TIER.name()); rate.setDestinationType("FBA");
+ rate.setDestinationCode("ONT8"); rate.setPricingTaxMode("TAX_INCLUDED"); rate.setChargeUnit("KG");
+ rate.setUnitPrice(new BigDecimal(unitPrice)); rate.setCurrency("USD"); rate.setAdditionalFee(BigDecimal.ZERO);
+ rate.setEffectiveFrom(NOW.minusDays(1));
+ return new ActivePriceBookEstimator.Estimate(701L, "MATSON_TRUCK", PriceModel.REGION_WEIGHT_TIER,
+ PricingTaxMode.TAX_INCLUDED, rate, new BigDecimal(quantity), "KG", new BigDecimal(unitPrice),
+ new BigDecimal(amount), "USD", new BigDecimal("167"), 6, "HALF_UP");
+ }
+
private InboundPricingIdempotencyEntity completed(String operation, long feeId) {
InboundPricingIdempotencyEntity value = new InboundPricingIdempotencyEntity();
value.setId(900L); value.setOperation(operation); value.setStatus("COMPLETED");