fix: support manual FBA quote fallback
This commit is contained in:
@@ -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(客户隔离键);费用明细引用平台公共费用项目',
|
||||
|
||||
+13
@@ -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:")
|
||||
|
||||
+1
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user