fix: support manual FBA quote fallback

This commit is contained in:
2026-09-21 10:44:23 +08:00
parent 8f2f485402
commit 9147190d67
9 changed files with 196 additions and 6 deletions
@@ -2286,8 +2286,8 @@ CREATE TABLE fl_inbound_fee (
fee_version int unsigned NOT NULL DEFAULT '1' COMMENT '费用快照版本',
charge_rule_id bigint unsigned DEFAULT NULL COMMENT '历史客户计费规则ID;渠道价格可为空',
charge_rule_version int unsigned DEFAULT NULL COMMENT '历史客户规则版本快照',
carrier_rate_id bigint unsigned NOT NULL COMMENT '船司FBA价格ID',
carrier_rate_version int unsigned NOT NULL COMMENT '船司价格版本快照',
carrier_rate_id bigint unsigned DEFAULT NULL COMMENT '船司FBA价格ID;人工报价为空',
carrier_rate_version int unsigned DEFAULT NULL COMMENT '船司价格版本快照;人工报价为空',
price_book_version_id bigint unsigned DEFAULT NULL COMMENT '整表价格版本ID快照',
rule_code varchar(50) DEFAULT NULL COMMENT '历史客户规则编码快照',
customer_id bigint unsigned DEFAULT NULL COMMENT '客户ID快照',
@@ -2309,7 +2309,7 @@ CREATE TABLE fl_inbound_fee (
destination_warehouse_id bigint unsigned DEFAULT NULL COMMENT '海外仓或FBA目的仓ID快照',
destination_name varchar(128) DEFAULT NULL COMMENT '目的仓名称快照',
destination_code varchar(32) DEFAULT NULL COMMENT 'FBA仓编码快照;海外仓可为空',
rate_effective_from datetime(3) NOT NULL COMMENT '价格生效开始快照',
rate_effective_from datetime(3) DEFAULT NULL COMMENT '价格生效开始快照;人工报价为空',
rate_effective_to datetime(3) DEFAULT NULL COMMENT '价格生效结束快照',
charge_mode varchar(16) DEFAULT NULL COMMENT '计费口径:KG或CBM',
conversion_factor decimal(18,6) DEFAULT NULL COMMENT '历史客户规则使用的换算系数快照',
@@ -2350,6 +2350,11 @@ CREATE TABLE fl_inbound_fee (
CONSTRAINT fk_fee_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='入库头程费用计算和确认快照';
ALTER TABLE fl_inbound_fee
MODIFY COLUMN carrier_rate_id bigint unsigned DEFAULT NULL COMMENT '船司FBA价格ID;人工报价为空',
MODIFY COLUMN carrier_rate_version int unsigned DEFAULT NULL COMMENT '船司价格版本快照;人工报价为空',
MODIFY COLUMN rate_effective_from datetime(3) DEFAULT NULL COMMENT '价格生效开始快照;人工报价为空';
CREATE TABLE fl_inbound_fee_detail (
id bigint unsigned NOT NULL COMMENT '入库费用明细ID',
tenant_id bigint unsigned NOT NULL COMMENT '客户ID(客户隔离键);费用明细引用平台公共费用项目',
@@ -8,6 +8,7 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.FbaPricingDtos.CalculateRequest;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest;
import com.baiyue.firstleg.dto.FbaPricingDtos.ManualQuoteRequest;
import com.baiyue.firstleg.service.FbaPricingService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import io.swagger.v3.oas.annotations.Operation;
@@ -50,6 +51,18 @@ public class PlatformFbaPricingController {
request == null ? null : request.volumeSource(), idempotencyKey));
}
@PostMapping("/manual-quote")
@PreAuthorize("hasAuthority('firstleg:inbound:confirm-send-fee')")
@BusinessAudit(event = "FBA_FEE_MANUAL_QUOTE", resourceType = "INBOUND", resourceId = "#inboundId",
description = "价格未命中时人工确认头程运费单价")
public ApiResponse<FeeSnapshotResponse> manualQuote(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody ManualQuoteRequest request) {
long scope = FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.manualQuote(scope, user.userId(), inboundId, request, idempotencyKey));
}
@PostMapping("/confirm")
@PreAuthorize("hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
+ " 'firstleg:fba-pricing:confirm')")
@@ -76,6 +76,13 @@ public final class FbaPricingDtos {
}
}
public record ManualQuoteRequest(
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal selectedWeightKg,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal selectedVolumeCbm,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal unitPrice,
@NotBlank @Size(min = 2, max = 500) String reason
) {}
public record AdjustFeeRequest(
@NotNull @Positive Integer expectedFeeVersion,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal chargeableQuantity,
@@ -7,6 +7,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
import com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest;
import com.baiyue.firstleg.dto.FbaPricingDtos.ManualQuoteRequest;
import com.baiyue.firstleg.domain.PricingDestinationType;
import com.baiyue.firstleg.domain.PricingTaxMode;
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest;
@@ -46,6 +47,7 @@ public class FbaPricingService {
private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
private static final long AUTOMATIC_CONFIRM_OPERATOR_ID = 0L;
private static final String CONTAINER_RECALCULATE_OPERATION = "CONTAINER_FEE_RECALCULATE";
private static final String MANUAL_RATE_CODE = "MANUAL_QUOTE";
@Resource
private FbaPricingRepository repository;
@Resource
@@ -177,6 +179,94 @@ public class FbaPricingService {
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
}
@Transactional
public FeeSnapshotResponse manualQuote(long tenantId, long operatorId, long inboundId,
ManualQuoteRequest request, String idempotencyKey) {
String reason = request.reason().trim();
if (reason.length() < 2 || request.unitPrice().signum() <= 0
|| request.selectedWeightKg().signum() <= 0 || request.selectedVolumeCbm().signum() <= 0) {
throw new BizException(ResultCode.VALIDATION_FAILED, "人工定价的单价、计费重量、体积和原因必须有效");
}
String discriminator = request.selectedWeightKg() + ":" + request.selectedVolumeCbm()
+ ":" + request.unitPrice() + ":" + reason;
InboundPricingIdempotencyEntity token = beginIdempotency(tenantId, operatorId, inboundId,
"FEE_MANUAL_QUOTE", idempotencyKey, hash("MANUAL_QUOTE", inboundId, discriminator));
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
if (!"FBA".equals(inbound.getDestinationType())) throw conflict("仅FBA头程运费支持未命中人工定价");
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
if (measurement == null || measurementRepository.listItems(tenantId, measurement.getId()).isEmpty()) {
throw conflict("实际测量明细不存在,不能生成人工头程费用");
}
InboundFeeEntity current = repository.findLatestFeeForUpdate(tenantId, inboundId);
if (feeRevisionLocked(current)) throw conflict("客户已确认,不允许再修改费用");
if (current != null && !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
throw conflict("当前费用状态不允许重新定价");
}
LocalDateTime now = LocalDateTime.now(clock);
PricingPolicy policy = pricingPolicy(inbound, now);
try {
priceBookEstimator.estimate(tenantId, new EstimateRequest(
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
pricingTaxMode(inbound), policy.chargeMode(), request.selectedWeightKg(), request.selectedVolumeCbm()));
throw conflict("当前条件已有匹配价格,请使用正常试算并调整单价");
} catch (BizException exception) {
if (exception.getResultCode() != ResultCode.NOT_FOUND) throw exception;
}
BigDecimal weight = request.selectedWeightKg().setScale(3, RoundingMode.HALF_UP);
BigDecimal volume = request.selectedVolumeCbm().setScale(4, RoundingMode.HALF_UP);
String unit = policy.chargeMode();
BigDecimal factor = new BigDecimal("KG".equals(unit) ? "167" : "363");
BigDecimal quantity = ("KG".equals(unit) ? weight.max(volume.multiply(factor))
: volume.max(weight.divide(factor, 12, RoundingMode.HALF_UP)))
.setScale(6, RoundingMode.HALF_UP);
BigDecimal subtotal = quantity.multiply(request.unitPrice()).setScale(MONEY_SCALE, RoundingMode.HALF_UP);
int version = repository.nextFeeVersion(tenantId, inboundId);
InboundFeeEntity fee = new InboundFeeEntity();
fee.setTenantId(tenantId); fee.setInboundId(inboundId);
fee.setFeeNo("FBAF-" + inboundId + "-" + version); fee.setFeeVersion(version);
fee.setMeasurementId(measurement.getId()); fee.setMeasurementSource("ACTUAL");
fee.setCarrierRateId(null); fee.setCarrierRateVersion(null); fee.setPriceBookVersionId(null);
fee.setRateCode(MANUAL_RATE_CODE); fee.setRateEffectiveFrom(null); fee.setRateEffectiveTo(null);
fee.setCustomerId(tenantId); fee.setCustomerName(inbound.getCustomerName());
fee.setBusinessType(inbound.getBusinessType()); fee.setTaxMode(inbound.getTaxMode());
fee.setPricingTaxMode(inbound.getPricingTaxMode()); fee.setOriginWarehouseId(inbound.getWarehouseId());
fee.setOriginWarehouseName(inbound.getWarehouseName()); fee.setChannelCode(inbound.getChannelCode());
fee.setDestinationType(inbound.getDestinationType()); fee.setDestinationWarehouseId(inbound.getDestinationWarehouseId());
fee.setDestinationName(inbound.getDestinationName()); fee.setDestinationCode(inbound.getDestinationCode());
fee.setChargeMode(unit); fee.setChargeUnit(unit); fee.setConversionFactor(factor);
fee.setRoundingScale(6); fee.setRoundingMode(RoundingMode.HALF_UP.name());
fee.setActualWeightKg(weight); fee.setActualVolumeCbm(volume); fee.setConvertedQuantity(quantity);
fee.setBaselineChargeableQuantity(quantity); fee.setTolerancePercent(ADJUST_TOLERANCE_PERCENT);
fee.setChargeableQuantity(quantity); fee.setCurrency("CNY");
fee.setSubtotalAmount(subtotal); fee.setAdditionalAmount(BigDecimal.ZERO.setScale(MONEY_SCALE));
fee.setTotalAmount(subtotal); fee.setFeeStatus("CALCULATED"); fee.setReviewStatus("NOT_SUBMITTED");
fee.setExceptionRelease(false); fee.setCalculatedBy(operatorId); fee.setCalculatedAt(now);
fee.setCreatedAt(now); fee.setUpdatedAt(now);
if (policy.rule() != null) {
fee.setChargeRuleId(policy.rule().getId()); fee.setChargeRuleVersion(policy.rule().getRuleVersion());
fee.setRuleCode(policy.rule().getRuleCode()); fee.setRuleEffectiveFrom(policy.rule().getEffectiveFrom());
fee.setRuleEffectiveTo(policy.rule().getEffectiveTo());
}
if (current != null && repository.supersedeCurrent(tenantId, current.getId(), current.getFeeStatus()) != 1) {
throw conflict("费用已被其他操作员修改,请刷新后重试");
}
repository.insertFee(fee);
InboundFeeItemEntity item = item(tenantId, fee, 1, "FBA_BASE", "FBA基础运费", subtotal, now);
item.setQuantity(quantity); item.setUnit(unit); item.setUnitPrice(request.unitPrice());
item.setAutomaticFlag(false); item.setRemark(reason);
repository.insertItem(item);
if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(), unit,
weight, volume, quantity) != 1) throw conflict("入库单状态已变化,费用未保存");
feeDetails.materializeCalculatedFreight(tenantId, inboundId, fee.getId(), operatorId, now);
complete(token, fee.getId(), now);
String nextStatus = preserveProcessStatusDuringPricing(inbound.getProcessStatus())
? inbound.getProcessStatus() : "FEE_CONFIRMING";
return response(inbound, fee, request.unitPrice(), nextStatus);
}
private ActivePriceBookEstimator.Estimate applyContainerCustomerTier(
InboundPricingEntity inbound, ActivePriceBookEstimator.Estimate individual,
BigDecimal currentWeight, BigDecimal currentVolume) {
@@ -280,6 +370,7 @@ public class FbaPricingService {
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.REPLAYED, 0);
}
List<ContainerFeeMember> regionalMembers = members.stream()
.filter(member -> member.current().getCarrierRateId() != null)
.filter(member -> priceModel(member.current()) == PriceModel.REGION_WEIGHT_TIER)
.toList();
List<ContainerFeeMember> unlockedRegionalMembers = regionalMembers.stream()
@@ -665,8 +665,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本',
charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空',
charge_rule_version INT UNSIGNED NULL COMMENT '历史客户规则版本快照',
carrier_rate_id BIGINT UNSIGNED NOT NULL COMMENT '船司FBA价格ID',
carrier_rate_version INT UNSIGNED NOT NULL COMMENT '船司价格版本快照',
carrier_rate_id BIGINT UNSIGNED NULL COMMENT '船司FBA价格ID;人工报价为空',
carrier_rate_version INT UNSIGNED NULL COMMENT '船司价格版本快照;人工报价为空',
price_book_version_id BIGINT UNSIGNED NULL COMMENT '整表价格版本ID快照',
rule_code VARCHAR(50) NULL COMMENT '历史客户规则编码快照',
customer_id BIGINT UNSIGNED NULL COMMENT '可选业务客户快照;平台公共渠道计费不依赖该字段',
@@ -688,7 +688,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
destination_warehouse_id BIGINT UNSIGNED NULL COMMENT '海外仓或FBA目的仓ID快照',
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
destination_code VARCHAR(32) NULL COMMENT 'FBA仓编码快照;海外仓可为空',
rate_effective_from DATETIME(3) NOT NULL COMMENT '价格生效开始快照',
rate_effective_from DATETIME(3) NULL COMMENT '价格生效开始快照;人工报价为空',
rate_effective_to DATETIME(3) NULL COMMENT '价格生效结束快照',
charge_mode VARCHAR(16) NULL COMMENT '计费口径:KG或CBM',
conversion_factor DECIMAL(18,6) NULL COMMENT '历史客户规则使用的换算系数快照',
@@ -726,6 +726,11 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
CONSTRAINT fk_fee_carrier_rate FOREIGN KEY (carrier_rate_id) REFERENCES fl_carrier_fba_rate (id)
) ENGINE=InnoDB COMMENT='入库头程费用计算和确认快照';
ALTER TABLE fl_inbound_fee
MODIFY COLUMN carrier_rate_id BIGINT UNSIGNED NULL COMMENT '船司FBA价格ID;人工报价为空',
MODIFY COLUMN carrier_rate_version INT UNSIGNED NULL COMMENT '船司价格版本快照;人工报价为空',
MODIFY COLUMN rate_effective_from DATETIME(3) NULL COMMENT '价格生效开始快照;人工报价为空';
SET @fee_baseline_quantity_sql = IF(
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='fl_inbound_fee' AND column_name='baseline_chargeable_quantity'),
@@ -154,6 +154,13 @@ paths:
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/manual-quote:
post:
summary: FBA价格未命中时人工确定头程单价
description: 仅FBA且价格查询返回未命中时可用;按客户计费方式与选定测量重量、体积计算计费量,附加费为零。生成正式头程费用快照,后续复用确认与发送;需要 firstleg:inbound:confirm-send-fee 权限。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ManualQuoteRequest'}}}}
responses: {'200': {description: 成功或业务失败, content: {application/json: {schema: {$ref: '#/components/schemas/FeeSnapshotApiResponse'}}}}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
post:
summary: 确认最新费用快照
@@ -488,6 +495,14 @@ components:
additionalAmount: {type: number, minimum: 0, multipleOf: 0.01}
reason: {type: string, minLength: 1, maxLength: 500}
toleranceConfirmed: {type: boolean, enum: [true], description: 前端展示上下界并由操作员二次确认后固定传true;服务端仍会重新校验范围与版本}
ManualQuoteRequest:
type: object
required: [selectedWeightKg, selectedVolumeCbm, unitPrice, reason]
properties:
selectedWeightKg: {type: number, exclusiveMinimum: true, minimum: 0}
selectedVolumeCbm: {type: number, exclusiveMinimum: true, minimum: 0}
unitPrice: {type: number, exclusiveMinimum: true, minimum: 0}
reason: {type: string, minLength: 2, maxLength: 500}
CalculateRequest:
type: object
properties:
@@ -25,6 +25,7 @@ class FbaPricingContractTest {
+ " 'firstleg:inbound:send-fee')");
assertPricingAuthority("adjustPublished", "hasAnyAuthority('firstleg:fba-pricing:adjust',"
+ " 'firstleg:inbound:confirm-send-fee')");
assertPricingAuthority("manualQuote", "hasAuthority('firstleg:inbound:confirm-send-fee')");
}
@Test
@@ -60,6 +61,7 @@ class FbaPricingContractTest {
.contains("/api/platform/v1/fba-pricing/customer-rules/{id}/default:")
.contains("/api/platform/v1/fba-pricing/carrier-rates/versions:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/calculate:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/manual-quote:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/confirm:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
@@ -26,6 +26,7 @@ class InboundFeeOpenApiContractTest {
Map<String, Object> paths = map(document.get("paths"));
assertThat(paths).containsKeys(
"/api/platform/v1/inbounds/{inboundId}/fees/calculate",
"/api/platform/v1/inbounds/{inboundId}/fees/manual-quote",
"/api/platform/v1/inbounds/{inboundId}/fees/confirm",
"/api/platform/v1/inbounds/{inboundId}/fees/send",
"/api/platform/v1/inbounds/{inboundId}/fees/latest",
@@ -5,6 +5,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.domain.PriceModel;
import com.baiyue.firstleg.domain.PricingTaxMode;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.dto.FbaPricingDtos.ManualQuoteRequest;
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
import com.baiyue.firstleg.entity.InboundFeeEntity;
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
@@ -365,6 +366,56 @@ class FbaPricingServiceTest {
verify(repository, never()).insertFee(any());
}
@Test
@DisplayName("FBA价格未命中时按人工单价生成零附加费头程费用")
void createsManualQuoteWhenFbaRateIsMissing() {
readyRepository();
InboundPricingEntity inbound = inbound("62.500", "0.3840");
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(301L);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(item));
when(priceBookEstimator.estimate(anyLong(), any())).thenThrow(
new BizException(ResultCode.NOT_FOUND, "当前条件没有匹配价格"));
FeeSnapshotResponse result = service.manualQuote(TENANT, OPERATOR, INBOUND,
new ManualQuoteRequest(new BigDecimal("62.500"), new BigDecimal("0.3840"),
new BigDecimal("12.2000"), "FBA价格未维护"), "manual-1");
assertThat(result.rateCode()).isEqualTo("MANUAL_QUOTE");
assertThat(result.unitPrice()).isEqualByComparingTo("12.2000");
assertThat(result.chargeableQuantity()).isEqualByComparingTo("64.128000");
assertThat(result.additionalAmount()).isEqualByComparingTo("0.00");
assertThat(result.totalAmount()).isEqualByComparingTo("782.36");
assertThat(result.carrierRateId()).isNull();
ArgumentCaptor<InboundFeeItemEntity> feeItem = ArgumentCaptor.forClass(InboundFeeItemEntity.class);
verify(repository).insertItem(feeItem.capture());
assertThat(feeItem.getValue().getAutomaticFlag()).isFalse();
assertThat(feeItem.getValue().getRemark()).isEqualTo("FBA价格未维护");
}
@Test
@DisplayName("已有匹配价格时拒绝绕过价格表创建人工报价")
void rejectsManualQuoteWhenRateExists() {
readyRepository();
InboundPricingEntity inbound = inbound("62.500", "0.3840");
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(301L);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(new InboundMeasurementItemEntity()));
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("KG", "64.128", "12.2", "782.36", "0"));
assertThatThrownBy(() -> service.manualQuote(TENANT, OPERATOR, INBOUND,
new ManualQuoteRequest(new BigDecimal("62.500"), new BigDecimal("0.3840"),
new BigDecimal("12.2000"), "人工价格"), "manual-existing"))
.isInstanceOf(BizException.class).hasMessageContaining("已有匹配价格");
verify(repository, never()).insertFee(any());
}
@Test
@DisplayName("同一幂等键重复试算直接返回原费用快照")
void returnsOriginalSnapshotForRepeatedCalculation() {