fix: support manual FBA quote fallback
This commit is contained in:
+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()
|
||||
|
||||
Reference in New Issue
Block a user