fix(firstleg): persist pricing conversion metadata
This commit is contained in:
@@ -69,7 +69,8 @@ public final class FbaPricingDtos {
|
||||
String businessType, String channelCode, Long carrierId, String carrierName,
|
||||
String destinationType, Long destinationWarehouseId, String destinationName, String destinationCode,
|
||||
String taxMode, String pricingTaxMode, String chargeMode,
|
||||
BigDecimal conversionFactor, Integer roundingScale, String roundingMode,
|
||||
@NotNull BigDecimal conversionFactor, @NotNull Integer roundingScale,
|
||||
@NotBlank String roundingMode,
|
||||
BigDecimal actualWeightKg, BigDecimal actualVolumeCbm,
|
||||
BigDecimal convertedQuantity, BigDecimal chargeableQuantity, String chargeUnit,
|
||||
BigDecimal unitPrice, BigDecimal subtotalAmount, BigDecimal additionalAmount,
|
||||
|
||||
@@ -69,7 +69,8 @@ public class ActivePriceBookEstimator {
|
||||
requireModel(rate, PriceModel.FBA_WAREHOUSE_RATE);
|
||||
var estimate = fbaEstimator.estimate(rate, request.actualWeightKg(), request.actualVolumeCbm());
|
||||
return new Estimate(versionId, channelCode, PriceModel.FBA_WAREHOUSE_RATE, request.pricingTaxMode(),
|
||||
rate, estimate.quantity(), estimate.unit(), estimate.unitPrice(), estimate.amount(), estimate.currency());
|
||||
rate, estimate.quantity(), estimate.unit(), estimate.unitPrice(), estimate.amount(), estimate.currency(),
|
||||
estimate.conversionFactor(), estimate.roundingScale(), estimate.roundingMode());
|
||||
}
|
||||
|
||||
private Estimate estimateRegion(long versionId, String channelCode, EstimateRequest request) {
|
||||
@@ -91,7 +92,8 @@ public class ActivePriceBookEstimator {
|
||||
var estimate = regionEstimator.estimate(tiers, request.actualWeightKg(), request.actualVolumeCbm());
|
||||
RateRow selected = estimate.rate();
|
||||
return new Estimate(versionId, channelCode, PriceModel.REGION_WEIGHT_TIER, request.pricingTaxMode(),
|
||||
selected, estimate.quantity(), "KG", estimate.unitPrice(), estimate.amount(), estimate.currency());
|
||||
selected, estimate.quantity(), "KG", estimate.unitPrice(), estimate.amount(), estimate.currency(),
|
||||
estimate.conversionFactor(), estimate.roundingScale(), estimate.roundingMode());
|
||||
}
|
||||
|
||||
private PriceRegion resolveWarehouseRegion(Long warehouseId) {
|
||||
@@ -146,6 +148,9 @@ public class ActivePriceBookEstimator {
|
||||
String chargeUnit,
|
||||
BigDecimal unitPrice,
|
||||
BigDecimal amount,
|
||||
String currency
|
||||
String currency,
|
||||
BigDecimal conversionFactor,
|
||||
int roundingScale,
|
||||
String roundingMode
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import java.math.RoundingMode;
|
||||
public class FbaWarehouseRateEstimator {
|
||||
private static final BigDecimal KG_PER_CBM = BigDecimal.valueOf(167);
|
||||
private static final BigDecimal KG_PER_BILLABLE_CBM = BigDecimal.valueOf(363);
|
||||
private static final int QUANTITY_SCALE = 6;
|
||||
private static final RoundingMode QUANTITY_ROUNDING_MODE = RoundingMode.HALF_UP;
|
||||
|
||||
public Estimate estimate(RateRow rate, BigDecimal actualWeightKg, BigDecimal actualVolumeCbm) {
|
||||
BigDecimal weight = nonNegative(actualWeightKg);
|
||||
@@ -19,14 +21,20 @@ public class FbaWarehouseRateEstimator {
|
||||
if (weight.signum() == 0 && volume.signum() == 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "重量和体积至少填写一项且必须大于0");
|
||||
}
|
||||
BigDecimal conversionFactor = switch (rate.getChargeUnit()) {
|
||||
case "KG" -> KG_PER_CBM;
|
||||
case "CBM" -> KG_PER_BILLABLE_CBM;
|
||||
default -> throw new BizException(ResultCode.CONFLICT, "当前价格计费单位不受支持");
|
||||
};
|
||||
BigDecimal quantity = switch (rate.getChargeUnit()) {
|
||||
case "KG" -> weight.max(volume.multiply(KG_PER_CBM));
|
||||
case "CBM" -> volume.max(weight.divide(KG_PER_BILLABLE_CBM, 12, RoundingMode.HALF_UP));
|
||||
default -> throw new BizException(ResultCode.CONFLICT, "当前价格计费单位不受支持");
|
||||
};
|
||||
quantity = quantity.setScale(6, RoundingMode.HALF_UP);
|
||||
quantity = quantity.setScale(QUANTITY_SCALE, QUANTITY_ROUNDING_MODE);
|
||||
return new Estimate(quantity, rate.getChargeUnit(), rate.getUnitPrice(),
|
||||
quantity.multiply(rate.getUnitPrice()).setScale(2, RoundingMode.HALF_UP), rate.getCurrency());
|
||||
quantity.multiply(rate.getUnitPrice()).setScale(2, RoundingMode.HALF_UP), rate.getCurrency(),
|
||||
conversionFactor, QUANTITY_SCALE, QUANTITY_ROUNDING_MODE.name());
|
||||
}
|
||||
|
||||
private static BigDecimal nonNegative(BigDecimal value) {
|
||||
@@ -36,5 +44,6 @@ public class FbaWarehouseRateEstimator {
|
||||
}
|
||||
|
||||
public record Estimate(BigDecimal quantity, String unit, BigDecimal unitPrice,
|
||||
BigDecimal amount, String currency) {}
|
||||
BigDecimal amount, String currency, BigDecimal conversionFactor,
|
||||
int roundingScale, String roundingMode) {}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.List;
|
||||
@Component
|
||||
public class RegionWeightTierEstimator {
|
||||
private static final BigDecimal KG_PER_CBM = BigDecimal.valueOf(167);
|
||||
private static final int QUANTITY_SCALE = 6;
|
||||
private static final RoundingMode QUANTITY_ROUNDING_MODE = RoundingMode.HALF_UP;
|
||||
|
||||
public Estimate estimate(List<RateRow> tiers, BigDecimal actualWeightKg, BigDecimal actualVolumeCbm) {
|
||||
BigDecimal weight = nonNegative(actualWeightKg);
|
||||
@@ -20,7 +22,8 @@ public class RegionWeightTierEstimator {
|
||||
if (weight.signum() == 0 && volume.signum() == 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "重量和体积至少填写一项且必须大于0");
|
||||
}
|
||||
BigDecimal quantity = weight.max(volume.multiply(KG_PER_CBM)).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal quantity = weight.max(volume.multiply(KG_PER_CBM))
|
||||
.setScale(QUANTITY_SCALE, QUANTITY_ROUNDING_MODE);
|
||||
RateRow selected = tiers.stream()
|
||||
.filter(rate -> "KG".equals(rate.getChargeUnit()) && rate.getWeightFromKg() != null)
|
||||
.filter(rate -> rate.getWeightFromKg().compareTo(quantity) <= 0)
|
||||
@@ -28,7 +31,8 @@ public class RegionWeightTierEstimator {
|
||||
.orElseThrow(() -> new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"当前计费重量未达到该海派渠道的最低重量阶梯"));
|
||||
return new Estimate(selected, quantity, selected.getUnitPrice(),
|
||||
quantity.multiply(selected.getUnitPrice()).setScale(2, RoundingMode.HALF_UP), selected.getCurrency());
|
||||
quantity.multiply(selected.getUnitPrice()).setScale(2, RoundingMode.HALF_UP), selected.getCurrency(),
|
||||
KG_PER_CBM, QUANTITY_SCALE, QUANTITY_ROUNDING_MODE.name());
|
||||
}
|
||||
|
||||
private static BigDecimal nonNegative(BigDecimal value) {
|
||||
@@ -38,5 +42,6 @@ public class RegionWeightTierEstimator {
|
||||
}
|
||||
|
||||
public record Estimate(RateRow rate, BigDecimal quantity, BigDecimal unitPrice, BigDecimal amount,
|
||||
String currency) {}
|
||||
String currency, BigDecimal conversionFactor, int roundingScale,
|
||||
String roundingMode) {}
|
||||
}
|
||||
|
||||
@@ -318,7 +318,8 @@ public class FbaPricingService {
|
||||
fee.setRuleCode(null); fee.setCustomerId(inbound.getCustomerId());
|
||||
fee.setCustomerName(inbound.getCustomerName()); fee.setBusinessType(inbound.getBusinessType());
|
||||
fee.setTaxMode(inbound.getTaxMode()); fee.setPricingTaxMode(inbound.getPricingTaxMode());
|
||||
fee.setRoundingScale(null); fee.setRoundingMode(null); fee.setRuleEffectiveFrom(null);
|
||||
fee.setRoundingScale(estimate.roundingScale()); fee.setRoundingMode(estimate.roundingMode());
|
||||
fee.setRuleEffectiveFrom(null);
|
||||
fee.setRuleEffectiveTo(null); fee.setRateCode(rate.getRateCode());
|
||||
fee.setOriginWarehouseId(rate.getOriginWarehouseId()); fee.setOriginWarehouseName(rate.getOriginWarehouseName());
|
||||
fee.setChannelCode(rate.getChannelCode()); fee.setCarrierId(null);
|
||||
@@ -326,7 +327,7 @@ public class FbaPricingService {
|
||||
fee.setDestinationWarehouseId(inbound.getDestinationWarehouseId()); fee.setDestinationName(inbound.getDestinationName());
|
||||
fee.setDestinationCode(inbound.getDestinationCode());
|
||||
fee.setRateEffectiveFrom(rate.getEffectiveFrom()); fee.setRateEffectiveTo(rate.getEffectiveTo());
|
||||
fee.setChargeMode(estimate.chargeUnit()); fee.setConversionFactor(null);
|
||||
fee.setChargeMode(estimate.chargeUnit()); fee.setConversionFactor(estimate.conversionFactor());
|
||||
fee.setActualWeightKg(weight); fee.setActualVolumeCbm(volume); fee.setConvertedQuantity(converted);
|
||||
fee.setChargeableQuantity(chargeable); fee.setChargeUnit(estimate.chargeUnit()); fee.setCurrency(rate.getCurrency());
|
||||
fee.setSubtotalAmount(subtotal); fee.setAdditionalAmount(additional); fee.setTotalAmount(total);
|
||||
|
||||
@@ -282,7 +282,7 @@ components:
|
||||
FeeSnapshotResponse:
|
||||
type: object
|
||||
description: fl_inbound_fee 当前版本快照;自动初算为 CALCULATED,人工确认后为 CONFIRMED,只有人工发布后为 SENT。
|
||||
required: [feeId, feeNo, feeVersion, feeStatus, inboundId, totalAmount, currency]
|
||||
required: [feeId, feeNo, feeVersion, feeStatus, inboundId, conversionFactor, roundingScale, roundingMode, totalAmount, currency]
|
||||
properties:
|
||||
feeId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
feeNo: {type: string}
|
||||
@@ -312,9 +312,9 @@ components:
|
||||
taxMode: {type: string, nullable: true}
|
||||
pricingTaxMode: {type: string, nullable: true}
|
||||
chargeMode: {type: string, nullable: true}
|
||||
conversionFactor: {type: number, nullable: true}
|
||||
roundingScale: {type: integer, nullable: true}
|
||||
roundingMode: {type: string, nullable: true}
|
||||
conversionFactor: {type: number, description: 本费用版本实际使用的重量体积换算系数;FBA/区域重量阶梯按KG为167,FBA按CBM为363}
|
||||
roundingScale: {type: integer, enum: [6], description: 本费用版本最终计费量实际保留的小数位数}
|
||||
roundingMode: {type: string, enum: [HALF_UP], description: 本费用版本最终计费量实际使用的舍入方式}
|
||||
actualWeightKg: {type: number, nullable: true}
|
||||
actualVolumeCbm: {type: number, nullable: true}
|
||||
convertedQuantity: {type: number, nullable: true}
|
||||
|
||||
@@ -85,6 +85,23 @@ class FbaPricingContractTest {
|
||||
.doesNotContain("format: int64");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("费用快照强制返回实际换算与计费量舍入参数")
|
||||
void requiresActualConversionAndRoundingMetadata() throws Exception {
|
||||
String source = new ClassPathResource("openapi/fba-pricing-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> schemas = map(map(new Yaml().load(source)).get("components"));
|
||||
Map<String, Object> fee = map(map(schemas.get("schemas")).get("FeeSnapshotResponse"));
|
||||
|
||||
assertThat(list(fee.get("required"))).contains(
|
||||
"conversionFactor", "roundingScale", "roundingMode");
|
||||
Map<String, Object> properties = map(fee.get("properties"));
|
||||
assertThat(map(properties.get("conversionFactor")).get("description").toString())
|
||||
.contains("167", "363");
|
||||
assertThat(list(map(properties.get("roundingScale")).get("enum"))).containsExactly(6);
|
||||
assertThat(list(map(properties.get("roundingMode")).get("enum"))).containsExactly("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户规则列表响应锁定数组包络和实体核心字段")
|
||||
void freezesCustomerRuleVersionListResponse() throws Exception {
|
||||
|
||||
+9
-3
@@ -22,15 +22,21 @@ class FbaWarehouseRateEstimatorTest {
|
||||
assertThat(estimate.unitPrice()).isEqualByComparingTo("12.50");
|
||||
assertThat(estimate.amount()).isEqualByComparingTo("417.50");
|
||||
assertThat(estimate.currency()).isEqualTo("CNY");
|
||||
assertThat(estimate.conversionFactor()).isEqualByComparingTo("167");
|
||||
assertThat(estimate.roundingScale()).isEqualTo(6);
|
||||
assertThat(estimate.roundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseGreaterOfVolumeAndWeightConversionForCbmRate() {
|
||||
var estimate = estimator.estimate(rate("CBM", "880"),
|
||||
new BigDecimal("363"), new BigDecimal("0.50"));
|
||||
new BigDecimal("37"), new BigDecimal("0.05"));
|
||||
|
||||
assertThat(estimate.quantity()).isEqualByComparingTo("1.000000");
|
||||
assertThat(estimate.amount()).isEqualByComparingTo("880.00");
|
||||
assertThat(estimate.quantity()).isEqualByComparingTo("0.101928");
|
||||
assertThat(estimate.amount()).isEqualByComparingTo("89.70");
|
||||
assertThat(estimate.conversionFactor()).isEqualByComparingTo("363");
|
||||
assertThat(estimate.roundingScale()).isEqualTo(6);
|
||||
assertThat(estimate.roundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -33,6 +33,9 @@ class RegionWeightTierEstimatorTest {
|
||||
assertThat(estimate.quantity()).isEqualByComparingTo("101.870000");
|
||||
assertThat(estimate.unitPrice()).isEqualByComparingTo("11");
|
||||
assertThat(estimate.amount()).isEqualByComparingTo("1120.57");
|
||||
assertThat(estimate.conversionFactor()).isEqualByComparingTo("167");
|
||||
assertThat(estimate.roundingScale()).isEqualTo(6);
|
||||
assertThat(estimate.roundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -84,6 +84,9 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.totalAmount()).isEqualByComparingTo("401.77");
|
||||
assertThat(response.priceBookVersionId()).isEqualTo(701L);
|
||||
assertThat(response.pricingTaxMode()).isEqualTo("TAX_INCLUDED");
|
||||
assertThat(response.conversionFactor()).isEqualByComparingTo("167");
|
||||
assertThat(response.roundingScale()).isEqualTo(6);
|
||||
assertThat(response.roundingMode()).isEqualTo("HALF_UP");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("FEE_CONFIRMING");
|
||||
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
|
||||
verify(repository).insertFee(fee.capture());
|
||||
@@ -92,6 +95,9 @@ class FbaPricingServiceTest {
|
||||
assertThat(fee.getValue().getChargeRuleId()).isNull();
|
||||
assertThat(fee.getValue().getCarrierId()).isNull();
|
||||
assertThat(fee.getValue().getCustomerId()).isEqualTo(11L);
|
||||
assertThat(fee.getValue().getConversionFactor()).isEqualByComparingTo("167");
|
||||
assertThat(fee.getValue().getRoundingScale()).isEqualTo(6);
|
||||
assertThat(fee.getValue().getRoundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -125,6 +131,9 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.convertedQuantity()).isEqualByComparingTo("1.0001");
|
||||
assertThat(response.chargeableQuantity()).isEqualByComparingTo("1.0001");
|
||||
assertThat(response.totalAmount()).isEqualByComparingTo("100.01");
|
||||
assertThat(response.conversionFactor()).isEqualByComparingTo("363");
|
||||
assertThat(response.roundingScale()).isEqualTo(6);
|
||||
assertThat(response.roundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -464,7 +473,8 @@ class FbaPricingServiceTest {
|
||||
rate.setAdditionalFee(new BigDecimal(additional)); rate.setEffectiveFrom(NOW.minusDays(1));
|
||||
return new ActivePriceBookEstimator.Estimate(701L, "MATSON_TRUCK", PriceModel.FBA_WAREHOUSE_RATE,
|
||||
PricingTaxMode.TAX_INCLUDED, rate, new BigDecimal(quantity), unit,
|
||||
new BigDecimal(price), new BigDecimal(amount), "USD");
|
||||
new BigDecimal(price), new BigDecimal(amount), "USD",
|
||||
"KG".equals(unit) ? new BigDecimal("167") : new BigDecimal("363"), 6, "HALF_UP");
|
||||
}
|
||||
|
||||
private InboundPricingIdempotencyEntity completed(String operation, long feeId) {
|
||||
|
||||
Reference in New Issue
Block a user