Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -243,13 +243,16 @@ public class FbaPricingRepository {
|
||||
.set(InboundFeeEntity::getSentAt, now));
|
||||
}
|
||||
|
||||
public int markInboundWaitingPallet(long tenantId, long inboundId) {
|
||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
public int markInboundFeeSent(long tenantId, long inboundId, String processStatus) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||
.eq(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING")
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.eq(InboundPricingEntity::getFeeStatus, "CONFIRMED")
|
||||
.set(InboundPricingEntity::getProcessStatus, "WAIT_PALLET")
|
||||
.set(InboundPricingEntity::getFeeStatus, "SENT"));
|
||||
.set(InboundPricingEntity::getFeeStatus, "SENT");
|
||||
if ("FEE_CONFIRMING".equals(processStatus)) {
|
||||
update.set(InboundPricingEntity::getProcessStatus, "WAIT_PALLET");
|
||||
}
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public InboundPricingIdempotencyEntity findIdempotency(long tenantId, long operatorId,
|
||||
|
||||
@@ -35,15 +35,14 @@ public class CartonMeasurementService {
|
||||
private static final Set<String> DEVICE_WRITABLE_STATUSES = Set.of("WAIT_MEASURE", "MEASURE_REJECTED");
|
||||
private static final Set<String> MANUAL_COUNT_EDITABLE_STATUSES = Set.of(
|
||||
"WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
|
||||
private static final String CALCULATION_RULE_VERSION = "V1_CBM_CEILING_4_GIRTH_L2WH";
|
||||
private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;
|
||||
|
||||
private final InboundMeasurementRepository repository;
|
||||
private final ObjectProvider<CartonLabelBindingResolver> resolverProvider;
|
||||
private final MeasurementImageClient imageClient;
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
private final CustomerDimensionWeightingService dimensionWeighting;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
@@ -51,11 +50,13 @@ public class CartonMeasurementService {
|
||||
public CartonMeasurementService(InboundMeasurementRepository repository,
|
||||
ObjectProvider<CartonLabelBindingResolver> resolverProvider,
|
||||
MeasurementImageClient imageClient,
|
||||
TransactionTemplate transactionTemplate) {
|
||||
TransactionTemplate transactionTemplate,
|
||||
CustomerDimensionWeightingService dimensionWeighting) {
|
||||
this.repository = repository;
|
||||
this.resolverProvider = resolverProvider;
|
||||
this.imageClient = imageClient;
|
||||
this.transactionTemplate = transactionTemplate;
|
||||
this.dimensionWeighting = dimensionWeighting;
|
||||
}
|
||||
|
||||
public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) {
|
||||
@@ -118,7 +119,7 @@ public class CartonMeasurementService {
|
||||
}
|
||||
requireVersion(expectedMeasurementVersion, measurement.getVersion());
|
||||
item.setPackageCount(confirmedPackageCount);
|
||||
applyCalculatedValues(item);
|
||||
applyCalculatedValuesFromSnapshot(item);
|
||||
item.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateItem(item)) {
|
||||
throw concurrentModification();
|
||||
@@ -187,7 +188,7 @@ public class CartonMeasurementService {
|
||||
item.setMachineNo(command.machineNo().trim());
|
||||
item.setDeviceMeasuredAt(command.measuredAt());
|
||||
item.setUpdatedAt(LocalDateTime.now());
|
||||
applyCalculatedValues(item);
|
||||
applyCalculatedValues(item, dimensionWeighting.load(tenantId));
|
||||
|
||||
if (created) {
|
||||
repository.insertItem(item);
|
||||
@@ -223,13 +224,15 @@ public class CartonMeasurementService {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
private void applyCalculatedValues(InboundMeasurementItemEntity item) {
|
||||
BigDecimal unitRaw = item.getLengthCm().multiply(item.getWidthCm()).multiply(item.getHeightCm())
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = unitRaw.setScale(4, RoundingMode.CEILING);
|
||||
private void applyCalculatedValues(InboundMeasurementItemEntity item,
|
||||
CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm());
|
||||
BigDecimal unitRaw = weighted.rawVolumeCbm();
|
||||
BigDecimal unitVolume = weighted.unitVolumeCbm();
|
||||
BigDecimal count = BigDecimal.valueOf(item.getPackageCount());
|
||||
BigDecimal totalRaw = unitRaw.multiply(count).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal totalVolume = unitVolume.multiply(count).setScale(4, RoundingMode.CEILING);
|
||||
BigDecimal totalVolume = unitVolume.multiply(count).setScale(4, RoundingMode.UNNECESSARY);
|
||||
item.setGirthCm(item.getLengthCm().add(item.getWidthCm().add(item.getHeightCm())
|
||||
.multiply(BigDecimal.valueOf(2))).setScale(3, RoundingMode.CEILING));
|
||||
item.setUnitWeightKg(item.getUnitWeightKg() == null ? null
|
||||
@@ -241,7 +244,31 @@ public class CartonMeasurementService {
|
||||
: item.getUnitWeightKg().multiply(count).setScale(3, RoundingMode.CEILING));
|
||||
item.setTotalVolumeCbm(totalVolume);
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
item.setCalculationRuleVersion(CALCULATION_RULE_VERSION);
|
||||
item.setCalculationRuleVersion(weighted.calculationRuleVersion());
|
||||
item.setOuterLengthCm(weighted.outerLengthCm());
|
||||
item.setOuterWidthCm(weighted.outerWidthCm());
|
||||
item.setOuterHeightCm(weighted.outerHeightCm());
|
||||
}
|
||||
|
||||
private void applyCalculatedValuesFromSnapshot(InboundMeasurementItemEntity item) {
|
||||
if (!positive(item.getOuterLengthCm()) || !positive(item.getOuterWidthCm())
|
||||
|| !positive(item.getOuterHeightCm()) || !positive(item.getUnitVolumeCbm())
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion())) {
|
||||
applyCalculatedValues(item, dimensionWeighting.load(item.getTenantId()));
|
||||
return;
|
||||
}
|
||||
BigDecimal count = BigDecimal.valueOf(item.getPackageCount());
|
||||
BigDecimal totalRaw = item.getOuterLengthCm().multiply(item.getOuterWidthCm())
|
||||
.multiply(item.getOuterHeightCm()).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP)
|
||||
.multiply(count).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal totalVolume = item.getUnitVolumeCbm().multiply(count)
|
||||
.setScale(4, RoundingMode.UNNECESSARY);
|
||||
item.setVolumeRawCbm(totalRaw);
|
||||
item.setVolumeCbm(totalVolume);
|
||||
item.setTotalWeightKg(item.getUnitWeightKg() == null ? null
|
||||
: item.getUnitWeightKg().multiply(count).setScale(3, RoundingMode.CEILING));
|
||||
item.setTotalVolumeCbm(totalVolume);
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
}
|
||||
|
||||
private void recalculateMeasurement(InboundMeasurementEntity measurement,
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 管理每个客户固定四种箱规的尺寸配置,不参与当前订单或计费计算。 */
|
||||
/** 管理每个客户固定四种箱规;规则供实测尺寸归档和计费体积加权使用,不包含价格。 */
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/** Resolves measured carton dimensions to the customer's smallest containing dimension rule. */
|
||||
@Service
|
||||
public class CustomerDimensionWeightingService {
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
private final CustomerDimensionConfigRepository repository;
|
||||
|
||||
public CustomerDimensionWeightingService(CustomerDimensionConfigRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public RuleSet load(long tenantId) {
|
||||
List<CustomerDimensionConfigEntity> values = repository.findAll(tenantId);
|
||||
if (values == null || values.size() != CustomerDimensionBoxType.values().length
|
||||
|| values.stream().anyMatch(CustomerDimensionWeightingService::invalidRule)
|
||||
|| values.stream().map(CustomerDimensionConfigEntity::getBoxType)
|
||||
.collect(java.util.stream.Collectors.toSet()).size() != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户箱规尺寸配置不完整,不能计算计费体积");
|
||||
}
|
||||
return new RuleSet(values.stream().map(Rule::from).toList());
|
||||
}
|
||||
|
||||
private static boolean invalidRule(CustomerDimensionConfigEntity value) {
|
||||
return value == null || value.getBoxType() == null || value.getVersion() == null
|
||||
|| !positive(value.getInnerLengthCm()) || !positive(value.getInnerWidthCm())
|
||||
|| !positive(value.getInnerHeightCm()) || !positive(value.getEdgeAdditionCm());
|
||||
}
|
||||
|
||||
private static boolean positive(BigDecimal value) {
|
||||
return value != null && value.signum() > 0;
|
||||
}
|
||||
|
||||
public static final class RuleSet {
|
||||
private final List<Rule> rules;
|
||||
|
||||
private RuleSet(List<Rule> rules) {
|
||||
this.rules = rules.stream()
|
||||
.sorted(Comparator.comparing(Rule::innerVolume)
|
||||
.thenComparing(rule -> rule.boxType().ordinal()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public WeightedDimensions resolve(BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm) {
|
||||
List<BigDecimal> measured = sortedDimensions(lengthCm, widthCm, heightCm);
|
||||
return rules.stream()
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(Rule::weighted)
|
||||
.orElseThrow(() -> new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"实测尺寸超出客户最大箱规,请先调整客户箱规后再提交测量"));
|
||||
}
|
||||
}
|
||||
|
||||
public record WeightedDimensions(
|
||||
CustomerDimensionBoxType boxType,
|
||||
Integer ruleVersion,
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
BigDecimal rawVolumeCbm,
|
||||
BigDecimal unitVolumeCbm,
|
||||
String calculationRuleVersion
|
||||
) {
|
||||
}
|
||||
|
||||
private record Rule(
|
||||
CustomerDimensionBoxType boxType,
|
||||
Integer version,
|
||||
BigDecimal innerLengthCm,
|
||||
BigDecimal innerWidthCm,
|
||||
BigDecimal innerHeightCm,
|
||||
BigDecimal edgeAdditionCm
|
||||
) {
|
||||
private static Rule from(CustomerDimensionConfigEntity value) {
|
||||
return new Rule(value.getBoxType(), value.getVersion(), value.getInnerLengthCm(),
|
||||
value.getInnerWidthCm(), value.getInnerHeightCm(), value.getEdgeAdditionCm());
|
||||
}
|
||||
|
||||
private BigDecimal innerVolume() {
|
||||
return innerLengthCm.multiply(innerWidthCm).multiply(innerHeightCm);
|
||||
}
|
||||
|
||||
private boolean contains(List<BigDecimal> measured) {
|
||||
List<BigDecimal> inner = sortedDimensions(innerLengthCm, innerWidthCm, innerHeightCm);
|
||||
return measured.get(0).compareTo(inner.get(0)) <= 0
|
||||
&& measured.get(1).compareTo(inner.get(1)) <= 0
|
||||
&& measured.get(2).compareTo(inner.get(2)) <= 0;
|
||||
}
|
||||
|
||||
private WeightedDimensions weighted() {
|
||||
BigDecimal edgePair = edgeAdditionCm.multiply(TWO);
|
||||
BigDecimal outerLength = innerLengthCm.add(edgePair);
|
||||
BigDecimal outerWidth = innerWidthCm.add(edgePair);
|
||||
BigDecimal outerHeight = innerHeightCm.add(edgePair);
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(boxType, version, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BigDecimal> sortedDimensions(BigDecimal length, BigDecimal width, BigDecimal height) {
|
||||
if (!positive(length) || !positive(width) || !positive(height)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "长、宽、高必须大于0");
|
||||
}
|
||||
List<BigDecimal> dimensions = new ArrayList<>(List.of(length, width, height));
|
||||
dimensions.sort(Comparator.reverseOrder());
|
||||
return dimensions;
|
||||
}
|
||||
}
|
||||
@@ -113,14 +113,16 @@ public class FbaPricingService {
|
||||
String hash = hash("SEND", inboundId, current.getId().toString());
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_SEND", idempotencyKey, hash);
|
||||
String nextProcessStatus = "INBOUNDED".equals(inbound.getProcessStatus())
|
||||
? "INBOUNDED" : "WAIT_PALLET";
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
feeDetails.publishFreight(tenantId, inboundId, token.getResultResourceId(),
|
||||
operatorId, null, LocalDateTime.now(clock));
|
||||
return response(tenantId, token.getResultResourceId(), "WAIT_PALLET");
|
||||
return response(tenantId, token.getResultResourceId(), nextProcessStatus);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.sendFee(tenantId, current.getId(), operatorId, now) != 1
|
||||
|| repository.markInboundWaitingPallet(tenantId, inboundId) != 1) {
|
||||
|| repository.markInboundFeeSent(tenantId, inboundId, inbound.getProcessStatus()) != 1) {
|
||||
throw conflict("费用未确认或入库单状态已变化,不能发送");
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, current.getId(), operatorId, null, now);
|
||||
@@ -135,7 +137,7 @@ public class FbaPricingService {
|
||||
event.setCreatedAt(now);
|
||||
repository.insertOutbox(event);
|
||||
complete(token, current.getId(), now);
|
||||
return response(tenantId, current.getId(), "WAIT_PALLET");
|
||||
return response(tenantId, current.getId(), nextProcessStatus);
|
||||
}
|
||||
|
||||
public FeeSnapshotResponse latest(long tenantId, long inboundId) {
|
||||
|
||||
@@ -35,10 +35,10 @@ public class MeasurementBatchService {
|
||||
/** 已接收第三方测量数据,待平台提交审核。 */
|
||||
static final String STATUS_RECEIVED = "RECEIVED";
|
||||
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
|
||||
static final String CALCULATION_RULE_VERSION = "V1_CBM_CEILING_4_GIRTH_L2WH";
|
||||
|
||||
@Resource
|
||||
private InboundMeasurementRepository repository;
|
||||
@Resource
|
||||
private CustomerDimensionWeightingService dimensionWeighting;
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
|
||||
@@ -47,7 +47,8 @@ public class MeasurementBatchService {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
}
|
||||
validateSource(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
|
||||
command, dimensionWeighting.load(command.tenantId()));
|
||||
|
||||
InboundMeasurementEntity measurement;
|
||||
if (command.measurementId() == null) {
|
||||
@@ -126,18 +127,20 @@ public class MeasurementBatchService {
|
||||
measurement.setActualGoodsName(command.actualGoodsName());
|
||||
}
|
||||
|
||||
private List<InboundMeasurementItemEntity> calculateItems(MeasurementBatchCommand command) {
|
||||
private List<InboundMeasurementItemEntity> calculateItems(
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
Set<String> itemNumbers = new HashSet<>();
|
||||
return command.items().stream().map(source -> {
|
||||
if (!itemNumbers.add(source.itemNo())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细号不能重复");
|
||||
}
|
||||
BigDecimal unitVolumeRaw = source.lengthCm().multiply(source.widthCm())
|
||||
.multiply(source.heightCm()).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = unitVolumeRaw.setScale(4, RoundingMode.CEILING);
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
|
||||
source.lengthCm(), source.widthCm(), source.heightCm());
|
||||
BigDecimal unitVolumeRaw = weighted.rawVolumeCbm();
|
||||
BigDecimal unitVolume = weighted.unitVolumeCbm();
|
||||
BigDecimal packageCount = BigDecimal.valueOf(source.packageCount());
|
||||
BigDecimal totalVolumeRaw = unitVolumeRaw.multiply(packageCount).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.CEILING);
|
||||
BigDecimal totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.UNNECESSARY);
|
||||
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setInboundDetailId(source.inboundDetailId());
|
||||
@@ -158,12 +161,12 @@ public class MeasurementBatchService {
|
||||
: source.unitWeightKg().multiply(packageCount).setScale(3, RoundingMode.CEILING));
|
||||
item.setTotalVolumeCbm(totalVolume);
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
item.setCalculationRuleVersion(CALCULATION_RULE_VERSION);
|
||||
item.setCalculationRuleVersion(weighted.calculationRuleVersion());
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
item.setOuterLengthCm(source.outerLengthCm());
|
||||
item.setOuterWidthCm(source.outerWidthCm());
|
||||
item.setOuterHeightCm(source.outerHeightCm());
|
||||
item.setOuterLengthCm(weighted.outerLengthCm());
|
||||
item.setOuterWidthCm(weighted.outerWidthCm());
|
||||
item.setOuterHeightCm(weighted.outerHeightCm());
|
||||
item.setRemark(source.remark());
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return item;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
-- 每个客户固定四条箱规尺寸配置;当前只维护配置事实,不接入订单或计费。
|
||||
-- 每个客户固定四条箱规尺寸配置;用于实测尺寸归档和计费体积加权,不包含价格。
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
|
||||
@@ -130,7 +130,7 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
|
||||
post:
|
||||
summary: 按审核通过测量快照试算 FBA 费用
|
||||
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
|
||||
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 INBOUNDED,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
|
||||
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: 成功或业务失败}, '403': {description: 无权限}}
|
||||
|
||||
@@ -32,7 +32,7 @@ paths:
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户固定四箱规尺寸配置
|
||||
description: 需要 firstleg:dimension-config:view 权限;按 SMALL、MEDIUM、LARGE、EXTRA_LARGE 固定顺序返回,当前不参与订单或计费。
|
||||
description: 需要 firstleg:dimension-config:view 权限;按 SMALL、MEDIUM、LARGE、EXTRA_LARGE 固定顺序返回;规则用于实测尺寸归档和计费体积加权,不包含价格。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ class CustomerDimensionConfigSqlContractTest {
|
||||
.contains("现有企业客户未完整持有固定四箱规尺寸配置");
|
||||
|
||||
String repeatSafeClause = sql.substring(backfill,
|
||||
sql.indexOf("DROP PROCEDURE IF EXISTS unified_assert_final_extensions", backfill));
|
||||
sql.indexOf("-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql", backfill));
|
||||
assertThat(repeatSafeClause)
|
||||
.doesNotContain("edge_addition_cm = VALUES")
|
||||
.doesNotContain("inner_length_cm = VALUES")
|
||||
|
||||
+34
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
@@ -150,6 +151,39 @@ class FbaPricingRepositoryRuleListTest {
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void feeSendFromInboundedOnlyChangesFeeStatus() {
|
||||
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus");
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "INBOUNDED", "CONFIRMED", "SENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void feeSendFromLegacyFlowAdvancesToWaitingPallet() {
|
||||
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "FEE_CONFIRMING")).isEqualTo(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").contains("processStatus");
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus");
|
||||
assertThat(params(wrapper.getValue()).values())
|
||||
.contains(10001L, 88L, "FEE_CONFIRMING", "CONFIRMED", "SENT", "WAIT_PALLET");
|
||||
}
|
||||
|
||||
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
|
||||
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
|
||||
+6
-2
@@ -90,11 +90,15 @@ class InboundParentChildSqlContractTest {
|
||||
|
||||
@Test
|
||||
void syncsUnifiedMigration() throws Exception {
|
||||
String module = normalize(sql).trim();
|
||||
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
|
||||
assertThat(unified)
|
||||
.contains("-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql")
|
||||
.contains(module);
|
||||
.contains("CREATE PROCEDURE phase3_inbound_add_column")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_batch")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_item")
|
||||
.contains("CREATE TRIGGER trg_inbound_order_parent_before_insert")
|
||||
.contains("CREATE PROCEDURE phase3_assert_inbound_parent_child_schema")
|
||||
.contains("旧订单唯一索引仍阻止创建子入仓单");
|
||||
}
|
||||
|
||||
private static String loadSql() {
|
||||
|
||||
+51
-6
@@ -5,10 +5,13 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -39,17 +42,22 @@ class CartonMeasurementServiceTest {
|
||||
private final CartonLabelBindingResolver resolver = mock(CartonLabelBindingResolver.class);
|
||||
private final MeasurementImageClient images = mock(MeasurementImageClient.class);
|
||||
private final TransactionTemplate transactions = mock(TransactionTemplate.class);
|
||||
private final CustomerDimensionConfigRepository dimensionConfigs = mock(CustomerDimensionConfigRepository.class);
|
||||
private final CustomerDimensionWeightingService dimensionWeighting =
|
||||
new CustomerDimensionWeightingService(dimensionConfigs);
|
||||
private CartonMeasurementService service;
|
||||
private final AtomicReference<InboundMeasurementItemEntity> insertedItem = new AtomicReference<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CartonMeasurementService(repository, resolverProvider, images, transactions);
|
||||
service = new CartonMeasurementService(repository, resolverProvider, images, transactions,
|
||||
dimensionWeighting);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
when(resolverProvider.getIfAvailable()).thenReturn(resolver);
|
||||
when(resolver.resolveByBoxCode(10001L, "RK-001#5-1"))
|
||||
.thenReturn(new CartonLabelBindingResolver.Binding(70001L, 80001L, "RK-001", 1, 5, "50x40x30cm"));
|
||||
when(repository.findInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
|
||||
when(dimensionConfigs.findAll(10001L)).thenReturn(dimensionRules(10001L));
|
||||
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
|
||||
when(images.upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()))
|
||||
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
|
||||
@@ -79,7 +87,10 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(1);
|
||||
assertThat(result.boxCode()).isEqualTo("RK-001#5-1");
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.0733");
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1870");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.000");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("54.000");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,7 +107,7 @@ class CartonMeasurementServiceTest {
|
||||
|
||||
assertThat(result.created()).isFalse();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(7);
|
||||
assertThat(existing.getLengthCm()).isEqualByComparingTo("60.100");
|
||||
assertThat(existing.getLengthCm()).isEqualByComparingTo("59.100");
|
||||
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
|
||||
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
|
||||
}
|
||||
@@ -139,6 +150,29 @@ class CartonMeasurementServiceTest {
|
||||
verify(repository).updateMeasurement(measurement);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedPackageCountPreservesTheRuleSnapshotAfterCustomerConfigChanges() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 2);
|
||||
InboundMeasurementItemEntity item = item(40001L, 30001L, 1, 99001L);
|
||||
item.setOuterLengthCm(new BigDecimal("34"));
|
||||
item.setOuterWidthCm(new BigDecimal("24"));
|
||||
item.setOuterHeightCm(new BigDecimal("24"));
|
||||
item.setUnitVolumeCbm(new BigDecimal("0.020"));
|
||||
item.setCalculationRuleVersion("CUSTOMER_DIMENSION_SMALL_V3");
|
||||
when(repository.findItem(10001L, 40001L)).thenReturn(item);
|
||||
when(repository.findMeasurement(10001L, 70001L, 30001L)).thenReturn(measurement);
|
||||
when(repository.updateItem(item)).thenReturn(true);
|
||||
when(repository.listItems(10001L, 30001L)).thenReturn(List.of(item));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
|
||||
service.updateConfirmedPackageCount(10001L, 70001L, 40001L, 2, 3);
|
||||
|
||||
assertThat(item.getTotalVolumeCbm()).isEqualByComparingTo("0.0600");
|
||||
assertThat(item.getOuterLengthCm()).isEqualByComparingTo("34");
|
||||
assertThat(item.getCalculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_SMALL_V3");
|
||||
verify(dimensionConfigs, never()).findAll(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedPackageCountCanBeCorrectedAfterSubmissionWithoutChangingOriginalPackageCount() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
@@ -158,13 +192,13 @@ class CartonMeasurementServiceTest {
|
||||
service.updateConfirmedPackageCount(10001L, 70001L, 40001L, 2, 5);
|
||||
|
||||
assertThat(inbound.getTotalPackageCount()).isEqualTo(5);
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("0.3000");
|
||||
assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("50.100");
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("0.5250");
|
||||
assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("87.675");
|
||||
verify(repository).updateInbound(inbound);
|
||||
}
|
||||
|
||||
private CartonMeasurementCommand command() {
|
||||
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("60.100"),
|
||||
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("59.100"),
|
||||
new BigDecimal("40.200"), new BigDecimal("30.300"), new BigDecimal("12.500"),
|
||||
"MJ-01", LocalDateTime.parse("2026-08-07T10:30:00"),
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
|
||||
@@ -193,4 +227,15 @@ class CartonMeasurementServiceTest {
|
||||
item.setSourceImageFileId(imageId); item.setItemNo("B1"); item.setBoxSpecification("50x40x30cm");
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<CustomerDimensionConfigEntity> dimensionRules(long tenantId) {
|
||||
return java.util.Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
|
||||
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
|
||||
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(0);
|
||||
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
|
||||
rule.setInnerHeightCm(type.defaultHeightCm());
|
||||
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
|
||||
return rule;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.pricing.FbaWarehouseRateEstimator;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/** Representative test-environment data flowing from dimension weighting into channel pricing. */
|
||||
class CustomerDimensionPricingFlowTest {
|
||||
private final CustomerDimensionConfigRepository configs = mock(CustomerDimensionConfigRepository.class);
|
||||
private final CustomerDimensionWeightingService dimensions = new CustomerDimensionWeightingService(configs);
|
||||
private final FbaWarehouseRateEstimator pricing = new FbaWarehouseRateEstimator();
|
||||
|
||||
@Test
|
||||
void channelRateChoosesWeightOrVolumeAfterCustomerDimensionWeighting() {
|
||||
when(configs.findAll(10001L)).thenReturn(defaultRules());
|
||||
var small = dimensions.load(10001L).resolve(
|
||||
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
|
||||
BigDecimal twoCartonsVolume = small.unitVolumeCbm().multiply(new BigDecimal("2"));
|
||||
BigDecimal actualWeight = new BigDecimal("24");
|
||||
|
||||
var kgEstimate = pricing.estimate(rate("KG", "8.700000"), actualWeight, twoCartonsVolume);
|
||||
var cbmEstimate = pricing.estimate(rate("CBM", "1400.000000"), actualWeight, twoCartonsVolume);
|
||||
|
||||
assertThat(twoCartonsVolume).isEqualByComparingTo("0.040");
|
||||
assertThat(kgEstimate.quantity()).isEqualByComparingTo("24.000000");
|
||||
assertThat(kgEstimate.amount()).isEqualByComparingTo("208.80");
|
||||
assertThat(cbmEstimate.quantity()).isEqualByComparingTo("0.066116");
|
||||
assertThat(cbmEstimate.amount()).isEqualByComparingTo("92.56");
|
||||
}
|
||||
|
||||
private RateRow rate(String unit, String price) {
|
||||
RateRow rate = new RateRow();
|
||||
rate.setChannelCode("EXX_TRUCK");
|
||||
rate.setDestinationType("OVERSEAS");
|
||||
rate.setDestinationWarehouseId(193000000000200003L);
|
||||
rate.setDestinationName("洛杉矶仓");
|
||||
rate.setChargeUnit(unit);
|
||||
rate.setUnitPrice(new BigDecimal(price));
|
||||
rate.setCurrency("CNY");
|
||||
return rate;
|
||||
}
|
||||
|
||||
private List<CustomerDimensionConfigEntity> defaultRules() {
|
||||
return Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
|
||||
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
|
||||
rule.setTenantId(10001L); rule.setBoxType(type); rule.setVersion(0);
|
||||
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
|
||||
rule.setInnerHeightCm(type.defaultHeightCm());
|
||||
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
|
||||
return rule;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CustomerDimensionWeightingServiceTest {
|
||||
private final CustomerDimensionConfigRepository repository = mock(CustomerDimensionConfigRepository.class);
|
||||
private final CustomerDimensionWeightingService service = new CustomerDimensionWeightingService(repository);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(repository.findAll(10001L)).thenReturn(defaultRules(10001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void measuredDimensionsUseSmallestContainingRuleAndAddEachEdgeTwice() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.019584");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.020");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonRotationDoesNotChangeTheMatchedRule() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("20"), new BigDecimal("30"), new BigDecimal("20"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossingOneSmallBoxBoundaryUsesTheNextContainingRule() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("30.001"), new BigDecimal("20"), new BigDecimal("20"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.MEDIUM);
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.051");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dimensionsBeyondTheLargestRuleAreRejected() {
|
||||
assertThatThrownBy(() -> service.load(10001L).resolve(
|
||||
new BigDecimal("60.001"), new BigDecimal("50"), new BigDecimal("50")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("超出客户最大箱规");
|
||||
}
|
||||
|
||||
@Test
|
||||
void incompleteCustomerRulesAreRejectedBeforeMeasurementCalculation() {
|
||||
when(repository.findAll(10001L)).thenReturn(defaultRules(10001L).subList(0, 3));
|
||||
|
||||
assertThatThrownBy(() -> service.load(10001L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("配置不完整");
|
||||
}
|
||||
|
||||
private List<CustomerDimensionConfigEntity> defaultRules(long tenantId) {
|
||||
return Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
|
||||
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
|
||||
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(3);
|
||||
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
|
||||
rule.setInnerHeightCm(type.defaultHeightCm());
|
||||
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
|
||||
return rule;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@@ -114,8 +114,6 @@ class FbaPricingServiceTest {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("100.000", "1.0000");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
|
||||
CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
estimate("KG", "167.000", "2.000000", "334.00", "0.00"));
|
||||
@@ -205,13 +203,15 @@ class FbaPricingServiceTest {
|
||||
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
|
||||
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("FEE_CONFIRMING");
|
||||
inbound.setFeeStatus("CONFIRMED");
|
||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(confirmed);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-once"))
|
||||
.thenReturn(null, completed("FEE_SEND", 501L));
|
||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundWaitingPallet(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, "FEE_CONFIRMING")).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
|
||||
@@ -219,14 +219,40 @@ class FbaPricingServiceTest {
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
|
||||
service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
FeeSnapshotResponse first = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
FeeSnapshotResponse repeated = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
|
||||
assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
verify(repository, times(1)).insertOutbox(any());
|
||||
verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW);
|
||||
verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已入库费用发送只发布费用且不回退主流程状态")
|
||||
void sendsFeeAfterInboundWithoutRollingBackProcessStatus() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setFeeStatus("CONFIRMED");
|
||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(confirmed);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-inbounded")).thenReturn(null);
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, "INBOUNDED")).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||
|
||||
FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
|
||||
verify(repository).insertOutbox(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同一幂等键用于不同试算时点会冲突")
|
||||
void rejectsReusedIdempotencyKeyWithDifferentRequest() {
|
||||
|
||||
+2
-3
@@ -3,7 +3,6 @@ package com.baiyue.firstleg.service;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
||||
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
|
||||
@@ -541,7 +540,7 @@ class InboundMeasurementServiceTest {
|
||||
saved.setVersion(0);
|
||||
saved.setStatus("DRAFT");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(orderCargoClient.detail(eq(""), eq(1L), eq(80L), eq(80001L))).thenReturn(
|
||||
when(orderCargoClient.detail(eq("internal-token"), eq(1L), eq(80L), eq(80001L))).thenReturn(
|
||||
ApiResponse.success(new OrderCargoClient.CargoBinding(80001L, 80L, 1, "Test cargo",
|
||||
2, new BigDecimal("6.500"), new BigDecimal("0.0480"))));
|
||||
when(batchService.saveBatch(any())).thenReturn(saved);
|
||||
@@ -556,7 +555,7 @@ class InboundMeasurementServiceTest {
|
||||
ArgumentCaptor<MeasurementBatchCommand> command = ArgumentCaptor.forClass(MeasurementBatchCommand.class);
|
||||
verify(batchService).saveBatch(command.capture());
|
||||
assertThat(command.getValue().items().get(0).inboundDetailId()).isEqualTo(80001L);
|
||||
verify(orderCargoClient).detail("", 1L, 80L, 80001L);
|
||||
verify(orderCargoClient).detail("internal-token", 1L, 80L, 80001L);
|
||||
}
|
||||
|
||||
private InboundOrderEntity inbound(String status) {
|
||||
|
||||
+26
-5
@@ -2,10 +2,13 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -27,14 +30,16 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("测量批次共享领域服务")
|
||||
@SpringJUnitConfig(MeasurementBatchService.class)
|
||||
@SpringJUnitConfig({MeasurementBatchService.class, CustomerDimensionWeightingService.class})
|
||||
class MeasurementBatchServiceTest {
|
||||
@MockBean private InboundMeasurementRepository repository;
|
||||
@MockBean private CustomerDimensionConfigRepository dimensionConfigs;
|
||||
@Resource private MeasurementBatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound());
|
||||
when(dimensionConfigs.findAll(1L)).thenReturn(dimensionRules(1L));
|
||||
org.mockito.Mockito.doAnswer(invocation -> {
|
||||
InboundMeasurementEntity entity = invocation.getArgument(0);
|
||||
entity.setId(100L);
|
||||
@@ -57,8 +62,13 @@ class MeasurementBatchServiceTest {
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
assertThat(items.getValue().get(0).getUnitVolumeCbm()).isEqualByComparingTo("0.0240");
|
||||
assertThat(items.getValue().get(0).getTotalVolumeCbm()).isEqualByComparingTo("0.0480");
|
||||
assertThat(items.getValue().get(0).getUnitVolumeCbm()).isEqualByComparingTo("0.0510");
|
||||
assertThat(items.getValue().get(0).getTotalVolumeCbm()).isEqualByComparingTo("0.1020");
|
||||
assertThat(items.getValue().get(0).getOuterLengthCm()).isEqualByComparingTo("44.000");
|
||||
assertThat(items.getValue().get(0).getOuterWidthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(items.getValue().get(0).getOuterHeightCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(items.getValue().get(0).getCalculationRuleVersion())
|
||||
.isEqualTo("CUSTOMER_DIMENSION_MEDIUM_V0");
|
||||
assertThat(items.getValue().get(0).getGirthCm()).isEqualByComparingTo("140.000");
|
||||
assertThat(items.getValue().get(0).getUnitWeightKg()).isNull();
|
||||
verify(repository, never()).updateInbound(any());
|
||||
@@ -74,8 +84,8 @@ class MeasurementBatchServiceTest {
|
||||
InboundMeasurementEntity saved = service.saveBatch(command);
|
||||
|
||||
assertThat(saved.getStatus()).isEqualTo("DRAFT");
|
||||
assertThat(saved.getTotalVolumeCbm()).isEqualByComparingTo("0.0480");
|
||||
assertThat(saved.getVolumeWeightKg()).isEqualByComparingTo("8.016");
|
||||
assertThat(saved.getTotalVolumeCbm()).isEqualByComparingTo("0.1020");
|
||||
assertThat(saved.getVolumeWeightKg()).isEqualByComparingTo("17.034");
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
@@ -145,4 +155,15 @@ class MeasurementBatchServiceTest {
|
||||
inbound.setVersion(0);
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private List<CustomerDimensionConfigEntity> dimensionRules(long tenantId) {
|
||||
return java.util.Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
|
||||
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
|
||||
rule.setTenantId(tenantId); rule.setBoxType(type); rule.setVersion(0);
|
||||
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
|
||||
rule.setInnerHeightCm(type.defaultHeightCm());
|
||||
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
|
||||
return rule;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user