diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/FbaPricingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/FbaPricingDtos.java index dbec6b7cf..07ca2ac16 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/FbaPricingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/FbaPricingDtos.java @@ -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, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricing/ActivePriceBookEstimator.java b/firstleg/src/main/java/com/baiyue/firstleg/pricing/ActivePriceBookEstimator.java index 003b4085e..9eba2b66d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricing/ActivePriceBookEstimator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricing/ActivePriceBookEstimator.java @@ -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 ) {} } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimator.java b/firstleg/src/main/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimator.java index 897aef553..e03cb8822 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimator.java @@ -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) {} } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimator.java b/firstleg/src/main/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimator.java index 3b9d25f6e..a8423664c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimator.java @@ -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 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) {} } 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 025f1776b..ecd089459 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -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); diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index aa91a88be..ea26300ee 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -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} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java index a3362b6cc..893e6c381 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java @@ -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 schemas = map(map(new Yaml().load(source)).get("components")); + Map fee = map(map(schemas.get("schemas")).get("FeeSnapshotResponse")); + + assertThat(list(fee.get("required"))).contains( + "conversionFactor", "roundingScale", "roundingMode"); + Map 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 { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimatorTest.java index 7fdcef6cd..ac07b089e 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimatorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/pricing/FbaWarehouseRateEstimatorTest.java @@ -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 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimatorTest.java index 6c9af0572..1e9ffd5db 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimatorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/pricing/RegionWeightTierEstimatorTest.java @@ -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 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 ac3e44ae8..85000cd71 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -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 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) {