修改提交
This commit is contained in:
@@ -2749,15 +2749,18 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
total_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '本箱规总体积m3',
|
||||
volume_weight_kg DECIMAL(18,3) NOT NULL COMMENT '本箱规材积重kg',
|
||||
charge_rule_id BIGINT UNSIGNED NULL COMMENT '该测量明细绑定的客户计费规则ID',
|
||||
coefficient_mode VARCHAR(16) NULL COMMENT '系数模式快照:ADD或MULTIPLY',
|
||||
coefficient_mode VARCHAR(16) NULL COMMENT '箱规计算策略快照:ACTUAL、ADD或MULTIPLY',
|
||||
calculation_rule_version VARCHAR(64) NOT NULL COMMENT '体积计算和舍入规则版本',
|
||||
source_image_file_id BIGINT UNSIGNED NULL COMMENT '测量设备图片文件ID',
|
||||
machine_no VARCHAR(64) NULL COMMENT '本次测量设备编号',
|
||||
device_measured_at DATETIME(3) NULL COMMENT '本次设备测量时间',
|
||||
over_limit_warning VARCHAR(500) NULL COMMENT '超线提醒快照',
|
||||
outer_length_cm DECIMAL(12,3) NULL COMMENT '外箱规长度cm',
|
||||
outer_width_cm DECIMAL(12,3) NULL COMMENT '外箱规宽度cm',
|
||||
outer_height_cm DECIMAL(12,3) NULL COMMENT '外箱规高度cm',
|
||||
outer_length_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规长度cm',
|
||||
outer_width_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规宽度cm',
|
||||
outer_height_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规高度cm',
|
||||
manual_outer_length_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规长度计算基数cm',
|
||||
manual_outer_width_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规宽度计算基数cm',
|
||||
manual_outer_height_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规高度计算基数cm',
|
||||
remark VARCHAR(500) NULL COMMENT '测量明细备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
@@ -3044,6 +3047,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
measurement_id BIGINT UNSIGNED NULL COMMENT '费用绑定的测量批次ID快照',
|
||||
measurement_source VARCHAR(32) NULL COMMENT '计费尺寸来源快照:OUTER为外箱规数据',
|
||||
fee_no VARCHAR(40) NOT NULL COMMENT '客户内唯一费用单号',
|
||||
fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本',
|
||||
charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空',
|
||||
@@ -3093,6 +3098,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
confirmed_at DATETIME(3) NULL COMMENT '确认时间',
|
||||
sent_by BIGINT UNSIGNED NULL COMMENT '发送人ID',
|
||||
sent_at DATETIME(3) NULL COMMENT '发送客户时间',
|
||||
customer_confirmed_by BIGINT UNSIGNED NULL COMMENT '客户确认人用户ID',
|
||||
customer_confirmed_by_name VARCHAR(64) NULL COMMENT '客户确认人名称快照',
|
||||
customer_confirmed_at DATETIME(3) NULL COMMENT '客户确认费用时间;非空后费用版本锁定',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
@@ -3337,6 +3345,12 @@ CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_width_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''外箱规宽度cm'' AFTER outer_length_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_height_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''外箱规高度cm'' AFTER outer_width_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_length_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规长度计算基数cm'' AFTER outer_height_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_width_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规宽度计算基数cm'' AFTER manual_outer_length_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_height_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规高度计算基数cm'' AFTER manual_outer_width_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'updated_at',
|
||||
'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT ''最后更新时间'' AFTER created_at');
|
||||
|
||||
@@ -3597,6 +3611,8 @@ CALL phase2_fba_add_column('fl_customer_charge_rule', 'height_factor', "DECIMAL(
|
||||
CALL phase2_fba_add_column('fl_customer_charge_rule', 'default_rule', "TINYINT(1) NOT NULL DEFAULT 0 COMMENT '客户默认生效规则' AFTER status");
|
||||
CALL phase2_fba_add_column('fl_carrier_fba_rate', 'version', "INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER deleted");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rule_code', "VARCHAR(50) NOT NULL DEFAULT '' COMMENT '客户规则编码快照' AFTER carrier_rate_version");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'measurement_id', "BIGINT UNSIGNED NULL COMMENT '费用绑定的测量批次ID快照' AFTER inbound_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'measurement_source', "VARCHAR(32) NULL COMMENT '计费尺寸来源快照:OUTER为外箱规数据' AFTER measurement_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_id', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '客户ID快照' AFTER rule_code");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_name', "VARCHAR(128) NOT NULL DEFAULT '' COMMENT '客户名称快照' AFTER customer_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'business_type', "VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型快照' AFTER customer_name");
|
||||
@@ -3614,6 +3630,9 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'carrier_name', "VARCHAR(128) NOT N
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'destination_code', "VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'FBA仓编码快照' AFTER carrier_name");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '价格生效开始快照' AFTER destination_code");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_by', "BIGINT UNSIGNED NULL COMMENT '客户确认人用户ID' AFTER sent_at");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_by_name', "VARCHAR(64) NULL COMMENT '客户确认人名称快照' AFTER customer_confirmed_by");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_at', "DATETIME(3) NULL COMMENT '客户确认费用时间;非空后费用版本锁定' AFTER customer_confirmed_by_name");
|
||||
CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json");
|
||||
CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键' AFTER warehouse_id");
|
||||
@@ -7946,8 +7965,8 @@ CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL' COMMENT '订单外箱规默认计算方式:ACTUAL不调整、ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
@@ -12997,7 +13016,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'bo
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_at', '创建时间');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_by', '创建人ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'deleted', '逻辑删除');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'edge_addition_cm', '长宽高各自增加一次的厘米数');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'edge_addition_cm', '每个尺寸方向的单侧加宽厘米数');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'default_calculation_mode', '订单外箱规默认计算方式:ADD或MULTIPLY');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'id', '配置ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'initialization_key', '创建流程幂等键及补偿归属');
|
||||
@@ -13292,9 +13311,12 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'ite
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'length_cm', '原始长度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'machine_no', '本次测量设备编号');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'measurement_id', '测量批次ID');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_height_cm', '外箱规高度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_length_cm', '外箱规长度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_width_cm', '外箱规宽度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_height_cm', '策略计算后的最终外箱规高度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_length_cm', '策略计算后的最终外箱规长度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_width_cm', '策略计算后的最终外箱规宽度cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'manual_outer_height_cm', '仓库人工外箱规高度计算基数cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'manual_outer_length_cm', '仓库人工外箱规长度计算基数cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'manual_outer_width_cm', '仓库人工外箱规宽度计算基数cm');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'over_limit_warning', '超线提醒快照');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'package_count', '相同箱规箱数');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'remark', '测量明细备注');
|
||||
|
||||
+21
-1
@@ -13,11 +13,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import jakarta.validation.Valid;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
|
||||
@Tag(name = "客户端入库结果与费用")
|
||||
@RestController
|
||||
@@ -31,12 +36,27 @@ public class MerchantInboundBillingController {
|
||||
@Operation(summary = "查询订单入库结果和最新费用",
|
||||
description = "客户按登录态客户范围只读查看;入库流程达到 WAIT_SPLIT 后,"
|
||||
+ "在 LOADING、LOADED、DELIVERED 仍持续可见,且始终要求 APPROVED + SENT "
|
||||
+ "并存在已发布入库运费明细,不需要客户确认费用。")
|
||||
+ "并存在已发布入库运费明细;同时返回当前费用版本的客户确认状态。")
|
||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderId}/inbound-billing/confirm")
|
||||
@PreAuthorize("hasAuthority('merchant:order:edit')")
|
||||
@Operation(summary = "客户确认已发布的入库费用",
|
||||
description = "仅允许当前客户确认 SENT 状态的最新费用版本;确认后该版本永久锁定,平台不得再修改。")
|
||||
public ApiResponse<FeeSnapshotResponse> confirm(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long orderId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody MerchantInboundBillingResponse.ConfirmFeeRequest request) {
|
||||
long tenantId = FirstlegScopeGuard.merchant(user);
|
||||
String operatorName = user.displayName() == null ? user.username() : user.displayName();
|
||||
return ApiResponse.success(service.confirmFee(tenantId, user.userId(), operatorName,
|
||||
orderId, request.expectedFeeVersion(), idempotencyKey));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/inbound-billing/images/{fileId}")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "查看已发布计费明细中的测量或称重图片")
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ public class PlatformFbaPricingController {
|
||||
return ApiResponse.success(service.calculate(scope, user.userId(), inboundId,
|
||||
request == null ? null : request.pricingAt(),
|
||||
request == null ? null : request.selectedWeightKg(),
|
||||
request == null ? null : request.selectedVolumeCbm(), idempotencyKey));
|
||||
request == null ? null : request.selectedVolumeCbm(),
|
||||
request == null ? null : request.weightSource(),
|
||||
request == null ? null : request.volumeSource(), idempotencyKey));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
|
||||
@@ -23,7 +23,7 @@ public final class CustomerDimensionConfigDtos {
|
||||
) { }
|
||||
|
||||
public record DefaultCalculationModeUpdateRequest(
|
||||
@NotBlank @Pattern(regexp = "ADD|MULTIPLY") String defaultCalculationMode
|
||||
@NotBlank @Pattern(regexp = "ACTUAL|ADD|MULTIPLY") String defaultCalculationMode
|
||||
) { }
|
||||
|
||||
public record RuleUpdateRequest(
|
||||
|
||||
@@ -86,6 +86,7 @@ public final class FbaPricingDtos {
|
||||
public record FeeSnapshotResponse(
|
||||
Long feeId, String feeNo, Integer feeVersion, String feeStatus,
|
||||
Long inboundId, String inboundCode,
|
||||
Long measurementId, String measurementSource,
|
||||
Long chargeRuleId, String ruleCode, Integer chargeRuleVersion,
|
||||
Long carrierRateId, String rateCode, Integer carrierRateVersion, Long priceBookVersionId,
|
||||
Long customerId, String customerName, Long originWarehouseId, String originWarehouseName,
|
||||
@@ -100,6 +101,8 @@ public final class FbaPricingDtos {
|
||||
BigDecimal totalAmount, String currency,
|
||||
LocalDateTime rateEffectiveFrom, LocalDateTime rateEffectiveTo,
|
||||
LocalDateTime calculatedAt, LocalDateTime confirmedAt, LocalDateTime sentAt,
|
||||
String nextProcessStatus
|
||||
String nextProcessStatus, String customerConfirmationStatus,
|
||||
Long customerConfirmedBy, String customerConfirmedByName,
|
||||
LocalDateTime customerConfirmedAt
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,14 @@ public final class InboundMeasurementRequests {
|
||||
@NotNull @DecimalMin(value = "0.001", message = "箱规高度必须大于等于 0.001 cm")
|
||||
@DecimalMax(value = "999999999.999", message = "箱规高度不能超过 999,999,999.999 cm")
|
||||
@Digits(integer = 9, fraction = 3, message = "箱规高度最多 9 位整数、3 位小数") BigDecimal heightCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 4) BigDecimal unitWeightKg,
|
||||
@Positive Long intelligentImageFileId,
|
||||
@Size(max = 500) String overLimitWarning,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerWidthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerHeightCm,
|
||||
@Positive Long chargeRuleId,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "ADD|MULTIPLY") String coefficientMode,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "ACTUAL|ADD|MULTIPLY") String coefficientMode,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public ItemRequest(Long inboundDetailId, String itemNo, String boxSpecification, Integer packageCount,
|
||||
@@ -65,7 +66,7 @@ public final class InboundMeasurementRequests {
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ public final class InboundMeasurementRequests {
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ public final class InboundMeasurementResponses {
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
BigDecimal calculatedOuterLengthCm,
|
||||
BigDecimal calculatedOuterWidthCm,
|
||||
BigDecimal calculatedOuterHeightCm,
|
||||
String calculationRuleVersion,
|
||||
Long chargeRuleId,
|
||||
String coefficientMode,
|
||||
String remark,
|
||||
|
||||
@@ -35,6 +35,7 @@ public record MeasurementBatchCommand(
|
||||
BigDecimal lengthCm,
|
||||
BigDecimal widthCm,
|
||||
BigDecimal heightCm,
|
||||
BigDecimal unitWeightKg,
|
||||
Long intelligentImageFileId,
|
||||
String overLimitWarning,
|
||||
BigDecimal outerLengthCm,
|
||||
@@ -44,13 +45,23 @@ public record MeasurementBatchCommand(
|
||||
String coefficientMode,
|
||||
String remark
|
||||
) {
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String coefficientMode, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, coefficientMode, remark);
|
||||
}
|
||||
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
@@ -60,7 +71,7 @@ public record MeasurementBatchCommand(
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
@@ -16,11 +18,14 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
BillingBasis billingBasis,
|
||||
FeeSnapshotResponse latestFee,
|
||||
PublishedFeeDetail publishedFeeDetail) {
|
||||
public record ConfirmFeeRequest(@NotNull @Positive Integer expectedFeeVersion) {
|
||||
}
|
||||
|
||||
public record MeasurementSummary(
|
||||
Long measurementId, String sourceType, String measurementStatus,
|
||||
Integer actualPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg,
|
||||
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName) {
|
||||
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName,
|
||||
String calculationSource) {
|
||||
}
|
||||
|
||||
public record ImageReference(
|
||||
@@ -32,7 +37,8 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
Long id, String itemNo, String boxSpecification, Integer packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal unitVolumeCbm, BigDecimal totalVolumeCbm,
|
||||
BigDecimal volumeWeightKg) {
|
||||
BigDecimal volumeWeightKg, Long chargeRuleId, String coefficientMode,
|
||||
String calculationRuleVersion) {
|
||||
}
|
||||
|
||||
public record WeightResult(
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 客户固定箱规的内径和长、宽、高尺寸增加值配置。 */
|
||||
/** 客户固定箱规的内径和每侧加宽配置。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_customer_dimension_config")
|
||||
|
||||
@@ -18,6 +18,8 @@ public class InboundFeeEntity {
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long measurementId;
|
||||
private String measurementSource;
|
||||
private String feeNo;
|
||||
private Integer feeVersion;
|
||||
private Long chargeRuleId;
|
||||
@@ -67,6 +69,9 @@ public class InboundFeeEntity {
|
||||
private LocalDateTime confirmedAt;
|
||||
private Long sentBy;
|
||||
private LocalDateTime sentAt;
|
||||
private Long customerConfirmedBy;
|
||||
private String customerConfirmedByName;
|
||||
private LocalDateTime customerConfirmedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public class InboundMeasurementItemEntity {
|
||||
private BigDecimal outerLengthCm;
|
||||
private BigDecimal outerWidthCm;
|
||||
private BigDecimal outerHeightCm;
|
||||
private BigDecimal manualOuterLengthCm;
|
||||
private BigDecimal manualOuterWidthCm;
|
||||
private BigDecimal manualOuterHeightCm;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -331,6 +331,20 @@ public class FbaPricingRepository {
|
||||
.set(InboundFeeEntity::getSentAt, now));
|
||||
}
|
||||
|
||||
public int confirmFeeByCustomer(long tenantId, long feeId, int expectedFeeVersion,
|
||||
long operatorId, String operatorName, LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
.eq(InboundFeeEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeEntity::getId, feeId)
|
||||
.eq(InboundFeeEntity::getFeeVersion, expectedFeeVersion)
|
||||
.eq(InboundFeeEntity::getFeeStatus, "SENT")
|
||||
.isNull(InboundFeeEntity::getCustomerConfirmedAt)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedBy, operatorId)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedByName, operatorName)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedAt, now)
|
||||
.set(InboundFeeEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public int voidSentFee(long tenantId, long inboundId, long feeId, int feeVersion,
|
||||
LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
|
||||
+8
-7
@@ -26,7 +26,8 @@ import java.util.Map;
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ADD";
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ACTUAL";
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
@Resource
|
||||
@@ -115,7 +116,7 @@ public class CustomerDimensionConfigService {
|
||||
requirePositive(rule.innerLengthCm(), "箱规内长");
|
||||
requirePositive(rule.innerWidthCm(), "箱规内宽");
|
||||
requirePositive(rule.innerHeightCm(), "箱规内高");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规尺寸增加值");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规单侧加宽");
|
||||
if (rule.version() == null || rule.version() < 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "箱规版本号无效");
|
||||
}
|
||||
@@ -222,10 +223,10 @@ public class CustomerDimensionConfigService {
|
||||
}
|
||||
|
||||
private static RuleResponse ruleResponse(CustomerDimensionConfigEntity value) {
|
||||
BigDecimal addition = value.getEdgeAdditionCm();
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(addition);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(addition);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(addition);
|
||||
BigDecimal edgePair = value.getEdgeAdditionCm().multiply(TWO);
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(edgePair);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(edgePair);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(edgePair);
|
||||
BigDecimal volume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
@@ -253,7 +254,7 @@ public class CustomerDimensionConfigService {
|
||||
private static String normalizeCalculationMode(String value) {
|
||||
if (value == null || value.isBlank()) return DEFAULT_CALCULATION_MODE;
|
||||
String normalized = value.trim().toUpperCase();
|
||||
if (!"ADD".equals(normalized) && !"MULTIPLY".equals(normalized)) {
|
||||
if (!"ACTUAL".equals(normalized) && !"ADD".equals(normalized) && !"MULTIPLY".equals(normalized)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "默认箱规计算方式无效");
|
||||
}
|
||||
return normalized;
|
||||
|
||||
+6
-15
@@ -16,6 +16,7 @@ import java.util.List;
|
||||
/** Resolves dimensions to the smallest containing rule, capped at the largest configured tier. */
|
||||
@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;
|
||||
@@ -60,7 +61,7 @@ public class CustomerDimensionWeightingService {
|
||||
return rules.stream()
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(Rule::weighted)
|
||||
.map(rule -> rule.weightedActualDimensions(lengthCm, widthCm, heightCm))
|
||||
.orElseGet(() -> rules.get(rules.size() - 1)
|
||||
.weightedActualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
@@ -122,22 +123,12 @@ public class CustomerDimensionWeightingService {
|
||||
&& measured.get(2).compareTo(inner.get(2)) <= 0;
|
||||
}
|
||||
|
||||
private WeightedDimensions weighted() {
|
||||
BigDecimal outerLength = innerLengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = innerWidthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = innerHeightCm.add(edgeAdditionCm);
|
||||
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, edgeAdditionCm, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
|
||||
private WeightedDimensions weightedActualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal outerLength = lengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = widthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = heightCm.add(edgeAdditionCm);
|
||||
BigDecimal edgePair = edgeAdditionCm.multiply(TWO);
|
||||
BigDecimal outerLength = lengthCm.add(edgePair);
|
||||
BigDecimal outerWidth = widthCm.add(edgePair);
|
||||
BigDecimal outerHeight = heightCm.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);
|
||||
|
||||
@@ -38,6 +38,8 @@ public class FbaPricingService {
|
||||
@Resource
|
||||
private FbaPricingRepository repository;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.repository.InboundMeasurementRepository measurementRepository;
|
||||
@Resource
|
||||
private ActivePriceBookEstimator priceBookEstimator;
|
||||
@Resource
|
||||
private Clock clock;
|
||||
@@ -56,15 +58,41 @@ public class FbaPricingService {
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, BigDecimal selectedWeightKg,
|
||||
BigDecimal selectedVolumeCbm, String idempotencyKey) {
|
||||
return calculate(tenantId, operatorId, inboundId, requestedAt, selectedWeightKg,
|
||||
selectedVolumeCbm, null, null, idempotencyKey);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, BigDecimal selectedWeightKg,
|
||||
BigDecimal selectedVolumeCbm, String weightSource,
|
||||
String volumeSource, String idempotencyKey) {
|
||||
String discriminator = (requestedAt == null ? "CURRENT" : requestedAt.toString()) + ":"
|
||||
+ (selectedWeightKg == null ? "ACTUAL_WEIGHT" : selectedWeightKg) + ":"
|
||||
+ (selectedVolumeCbm == null ? "ACTUAL_VOLUME" : selectedVolumeCbm);
|
||||
if (weightSource != null || volumeSource != null) {
|
||||
discriminator += ":" + (weightSource == null ? "UNSPECIFIED" : weightSource)
|
||||
+ ":" + (volumeSource == null ? "UNSPECIFIED" : volumeSource);
|
||||
}
|
||||
String hash = hash("CALCULATE", inboundId, discriminator);
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
||||
|
||||
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource) && measurement == null) {
|
||||
throw conflict("外箱规测量批次不存在,不能生成对客费用");
|
||||
}
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource)) {
|
||||
var outerItems = measurementRepository.listItems(tenantId, measurement.getId());
|
||||
boolean incomplete = outerItems.isEmpty() || outerItems.stream().anyMatch(item ->
|
||||
item.getOuterLengthCm() == null || item.getOuterLengthCm().signum() <= 0
|
||||
|| item.getOuterWidthCm() == null || item.getOuterWidthCm().signum() <= 0
|
||||
|| item.getOuterHeightCm() == null || item.getOuterHeightCm().signum() <= 0
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion()));
|
||||
if (incomplete) throw conflict("外箱规数据或绑定的计算规则不完整,不能生成对客费用");
|
||||
}
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
@@ -84,6 +112,8 @@ public class FbaPricingService {
|
||||
|
||||
InboundFeeEntity fee = snapshot(tenantId, operatorId, inbound, estimate, rate, version,
|
||||
weight, volume, converted, chargeable, subtotal, additional, total, now);
|
||||
fee.setMeasurementId(measurement == null ? null : measurement.getId());
|
||||
fee.setMeasurementSource("OUTER".equalsIgnoreCase(volumeSource) ? "OUTER" : volumeSource);
|
||||
repository.supersedeCalculated(tenantId, inboundId);
|
||||
repository.insertFee(fee);
|
||||
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
|
||||
@@ -102,6 +132,10 @@ public class FbaPricingService {
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirm(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())) {
|
||||
throw conflict("订单已进入装柜流程,不能再修改或发送费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无可确认的费用试算");
|
||||
String hash = hash("CONFIRM", inboundId, current.getId().toString());
|
||||
@@ -121,6 +155,9 @@ public class FbaPricingService {
|
||||
public FeeSnapshotResponse send(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())) {
|
||||
throw conflict("订单已进入装柜流程,不能再修改或发送费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无费用快照");
|
||||
String hash = hash("SEND", inboundId, current.getId().toString());
|
||||
@@ -151,6 +188,39 @@ public class FbaPricingService {
|
||||
return response(tenantId, fee.getId(), null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirmByCustomer(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, int expectedFeeVersion,
|
||||
String idempotencyKey) {
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null || !"SENT".equals(current.getFeeStatus())) {
|
||||
throw conflict("费用尚未发送给客户,不能确认");
|
||||
}
|
||||
if (!Integer.valueOf(expectedFeeVersion).equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重新确认");
|
||||
}
|
||||
String hash = hash("CUSTOMER_CONFIRM", inboundId,
|
||||
current.getId() + ":" + expectedFeeVersion);
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(tenantId, operatorId, inboundId,
|
||||
"CUSTOMER_FEE_CONFIRM", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
return response(tenantId, token.getResultResourceId(), null);
|
||||
}
|
||||
if (current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许重复确认");
|
||||
}
|
||||
String confirmedName = StringUtils.hasText(operatorName)
|
||||
? operatorName.trim() : String.valueOf(operatorId);
|
||||
if (confirmedName.length() > 64) confirmedName = confirmedName.substring(0, 64);
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.confirmFeeByCustomer(tenantId, current.getId(), expectedFeeVersion,
|
||||
operatorId, confirmedName, now) != 1) {
|
||||
throw conflict("客户已确认或费用版本已变化,请刷新后重试");
|
||||
}
|
||||
complete(token, current.getId(), now);
|
||||
return response(tenantId, current.getId(), null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse adjustPublished(long tenantId, long operatorId, long inboundId,
|
||||
AdjustFeeRequest request, String idempotencyKey) {
|
||||
@@ -163,6 +233,9 @@ public class FbaPricingService {
|
||||
if (current == null || !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的初算或已发布费用");
|
||||
}
|
||||
if (current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许再修改");
|
||||
}
|
||||
if (!request.expectedFeeVersion().equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重试");
|
||||
}
|
||||
@@ -237,7 +310,7 @@ public class FbaPricingService {
|
||||
|
||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())
|
||||
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|
||||
throw conflict("当前入库单状态不允许费用试算");
|
||||
}
|
||||
@@ -250,11 +323,6 @@ public class FbaPricingService {
|
||||
if (!"FBA".equals(inbound.getDestinationType()) && !"OVERSEAS".equals(inbound.getDestinationType())) {
|
||||
throw conflict("当前目的地类型不支持自动计费");
|
||||
}
|
||||
if (!"APPROVED".equals(inbound.getMeasureReviewStatus())) throw conflict("只能消费审核通过的测量快照");
|
||||
if (inbound.getActualNetWeightKg() == null || inbound.getActualNetWeightKg().signum() <= 0
|
||||
|| inbound.getActualVolumeCbm() == null || inbound.getActualVolumeCbm().signum() <= 0) {
|
||||
throw conflict("审核通过的测量快照必须包含大于零的货物净重和体积");
|
||||
}
|
||||
if (inbound.getCustomerId() == null || inbound.getCustomerId() <= 0) {
|
||||
throw conflict("入库单缺少订单客户归属,不能生成对客费用");
|
||||
}
|
||||
@@ -268,6 +336,11 @@ public class FbaPricingService {
|
||||
return "WAIT_SPLIT".equals(processStatus);
|
||||
}
|
||||
|
||||
private static boolean feeEditableBeforeLoading(String processStatus) {
|
||||
return List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(processStatus);
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
try {
|
||||
return PricingDestinationType.valueOf(inbound.getDestinationType());
|
||||
@@ -383,6 +456,7 @@ public class FbaPricingService {
|
||||
BigDecimal unitPrice, String nextStatus) {
|
||||
return new FeeSnapshotResponse(fee.getId(), fee.getFeeNo(), fee.getFeeVersion(), fee.getFeeStatus(),
|
||||
fee.getInboundId(), inbound == null ? null : inbound.getInboundCode(),
|
||||
fee.getMeasurementId(), fee.getMeasurementSource(),
|
||||
fee.getChargeRuleId(), fee.getRuleCode(), fee.getChargeRuleVersion(),
|
||||
fee.getCarrierRateId(), fee.getRateCode(), fee.getCarrierRateVersion(), fee.getPriceBookVersionId(),
|
||||
fee.getCustomerId(), fee.getCustomerName(), fee.getOriginWarehouseId(), fee.getOriginWarehouseName(),
|
||||
@@ -394,7 +468,9 @@ public class FbaPricingService {
|
||||
fee.getConvertedQuantity(), fee.getChargeableQuantity(), fee.getChargeUnit(), unitPrice,
|
||||
fee.getSubtotalAmount(), fee.getAdditionalAmount(), fee.getTotalAmount(), fee.getCurrency(),
|
||||
fee.getRateEffectiveFrom(), fee.getRateEffectiveTo(), fee.getCalculatedAt(), fee.getConfirmedAt(),
|
||||
fee.getSentAt(), nextStatus);
|
||||
fee.getSentAt(), nextStatus,
|
||||
fee.getCustomerConfirmedAt() == null ? "PENDING_CONFIRMATION" : "CONFIRMED",
|
||||
fee.getCustomerConfirmedBy(), fee.getCustomerConfirmedByName(), fee.getCustomerConfirmedAt());
|
||||
}
|
||||
|
||||
private static String hash(String operation, long inboundId, String discriminator) {
|
||||
|
||||
@@ -67,10 +67,22 @@ public class InboundMeasurementService {
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
InboundMeasurementEntity measurement = repository.findLatestMeasurement(tenantId, inboundId);
|
||||
return detail(tenantId, inbound, measurement);
|
||||
}
|
||||
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId, long measurementId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
InboundMeasurementEntity measurement = repository.findMeasurement(tenantId, inboundId, measurementId);
|
||||
if (measurement == null) throw new BizException(ResultCode.NOT_FOUND, "费用绑定的测量批次不存在");
|
||||
return detail(tenantId, inbound, measurement);
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Detail detail(long tenantId, InboundOrderEntity inbound,
|
||||
InboundMeasurementEntity measurement) {
|
||||
List<InboundMeasurementItemEntity> items = measurement == null
|
||||
? List.of() : repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inboundId);
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inbound.getId());
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration = loadLatestDeclaration(inbound);
|
||||
return toDetail(inbound, measurement, items, weights, review, latestDeclaration);
|
||||
}
|
||||
@@ -119,7 +131,7 @@ public class InboundMeasurementService {
|
||||
actualGoodsName,
|
||||
operatorId, request.items().stream().map(item -> new MeasurementBatchCommand.Item(
|
||||
item.inboundDetailId(), item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(),
|
||||
item.widthCm(), item.heightCm(), item.intelligentImageFileId(),
|
||||
item.widthCm(), item.heightCm(), item.unitWeightKg(), item.intelligentImageFileId(),
|
||||
item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(),
|
||||
item.chargeRuleId(), item.coefficientMode(), item.remark())).toList());
|
||||
InboundMeasurementEntity saved = batchService.saveBatch(command);
|
||||
@@ -154,9 +166,6 @@ public class InboundMeasurementService {
|
||||
inbound.setProcessStatus("MEASURE_PENDING");
|
||||
inbound.setMeasureReviewStatus("PENDING");
|
||||
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
||||
if (measurement.getTotalPackageCount() != null) {
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
}
|
||||
inbound.setActualGrossWeightKg(confirmedTotals.grossWeightKg());
|
||||
inbound.setActualNetWeightKg(confirmedTotals.netWeightKg());
|
||||
inbound.setPalletCount(confirmedTotals.palletCount());
|
||||
@@ -534,36 +543,21 @@ public class InboundMeasurementService {
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) {
|
||||
String overrideVersion = item.getCalculationRuleVersion();
|
||||
boolean legacyFullOverride = "WAREHOUSE_OUTER_OVERRIDE_V1".equals(overrideVersion);
|
||||
boolean lengthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'L');
|
||||
boolean widthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'W');
|
||||
boolean heightOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'H');
|
||||
return new InboundMeasurementResponses.Item(
|
||||
item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(),
|
||||
item.getUnitVolumeCbm(),
|
||||
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), lengthOverridden ? item.getOuterLengthCm() : null,
|
||||
widthOverridden ? item.getOuterWidthCm() : null,
|
||||
heightOverridden ? item.getOuterHeightCm() : null,
|
||||
item.getOverLimitWarning(), item.getManualOuterLengthCm(),
|
||||
item.getManualOuterWidthCm(), item.getManualOuterHeightCm(),
|
||||
item.getOuterLengthCm(), item.getOuterWidthCm(), item.getOuterHeightCm(),
|
||||
item.getCalculationRuleVersion(),
|
||||
item.getChargeRuleId(), item.getCoefficientMode(),
|
||||
item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getPackageCount(),
|
||||
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
private boolean warehouseOverrideContains(String calculationRuleVersion, char dimension) {
|
||||
if (calculationRuleVersion == null
|
||||
|| !calculationRuleVersion.startsWith("WAREHOUSE_OUTER_OVERRIDE_")
|
||||
|| !calculationRuleVersion.endsWith("_V1")) {
|
||||
return false;
|
||||
}
|
||||
String dimensions = calculationRuleVersion.substring(
|
||||
"WAREHOUSE_OUTER_OVERRIDE_".length(), calculationRuleVersion.length() - 3);
|
||||
return dimensions.indexOf(dimension) >= 0;
|
||||
}
|
||||
|
||||
private void requireConfirmedPackageTotal(InboundOrderEntity inbound,
|
||||
List<InboundMeasurementItemEntity> items) {
|
||||
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class MeasurementBatchService {
|
||||
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
|
||||
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
|
||||
private static final Pattern SPECIFICATION_COEFFICIENT_PATTERN = Pattern.compile(
|
||||
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)");
|
||||
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)(?:;重量:(\\d+(?:\\.\\d+)?))?");
|
||||
@Resource
|
||||
private InboundMeasurementRepository repository;
|
||||
@Resource
|
||||
@@ -167,7 +167,8 @@ public class MeasurementBatchService {
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId(), customerId) : null;
|
||||
String coefficientMode = coefficientRule != null ? "MULTIPLY" : requestedMode;
|
||||
boolean useDimensionRule = "ADD".equals(coefficientMode);
|
||||
BigDecimal addition = weighted.edgeAdditionCm();
|
||||
BigDecimal addition = weighted.edgeAdditionCm() == null
|
||||
? null : weighted.edgeAdditionCm().multiply(BigDecimal.valueOf(2));
|
||||
boolean hasMatchedAddition = useDimensionRule && weighted.boxType() != null;
|
||||
// ADD 只从匹配箱规取得加边值,外尺寸仍以本次实测尺寸为基准,
|
||||
// 不能把箱规边界尺寸当成本次货物的实际外尺寸。
|
||||
@@ -176,7 +177,7 @@ public class MeasurementBatchService {
|
||||
BigDecimal outerHeight = hasMatchedAddition ? baseHeight.add(addition) : baseHeight;
|
||||
if (coefficientRule != null) {
|
||||
BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
|
||||
coefficientRule, baseLength, baseWidth, baseHeight);
|
||||
coefficientRule, baseLength, baseWidth, baseHeight, source.unitWeightKg());
|
||||
outerLength = applyCoefficient(outerLength, specificationCoefficient);
|
||||
outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
|
||||
outerHeight = applyCoefficient(outerHeight, specificationCoefficient);
|
||||
@@ -222,13 +223,13 @@ public class MeasurementBatchService {
|
||||
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
// Override fields retain the operator's base values so repeated saves do not compound the strategy.
|
||||
item.setOuterLengthCm((source.outerLengthCm() != null ? source.outerLengthCm() : outerLength)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm((source.outerWidthCm() != null ? source.outerWidthCm() : outerWidth)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm((source.outerHeightCm() != null ? source.outerHeightCm() : outerHeight)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
// 人工外箱值是策略计算基数;最终外箱值单独落库,避免覆盖基数或重复叠加策略。
|
||||
item.setManualOuterLengthCm(source.outerLengthCm());
|
||||
item.setManualOuterWidthCm(source.outerWidthCm());
|
||||
item.setManualOuterHeightCm(source.outerHeightCm());
|
||||
item.setOuterLengthCm(outerLength.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm(outerWidth.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm(outerHeight.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setRemark(source.remark());
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return item;
|
||||
@@ -250,7 +251,7 @@ public class MeasurementBatchService {
|
||||
private String normalizeMode(String mode) {
|
||||
if (mode == null || mode.isBlank()) return null;
|
||||
String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!Set.of("ADD", "MULTIPLY").contains(normalized)) {
|
||||
if (!Set.of("ACTUAL", "ADD", "MULTIPLY").contains(normalized)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "系数模式仅支持 ADD 或 MULTIPLY");
|
||||
}
|
||||
return normalized;
|
||||
@@ -287,14 +288,14 @@ public class MeasurementBatchService {
|
||||
|
||||
private BigDecimal resolveSpecificationCoefficient(CustomerChargeRuleEntity rule,
|
||||
BigDecimal length, BigDecimal width,
|
||||
BigDecimal height) {
|
||||
BigDecimal height, BigDecimal unitWeightKg) {
|
||||
List<SpecificationCoefficient> tiers = parseSpecificationCoefficients(rule.getRemark());
|
||||
if (tiers.isEmpty()) {
|
||||
return rule.getLengthFactor() == null ? BigDecimal.ONE : rule.getLengthFactor();
|
||||
}
|
||||
List<BigDecimal> measured = sortedDimensions(length, width, height);
|
||||
return tiers.stream()
|
||||
.filter(tier -> tier.contains(measured))
|
||||
.filter(tier -> tier.contains(measured, unitWeightKg))
|
||||
.findFirst()
|
||||
.orElse(tiers.get(tiers.size() - 1))
|
||||
.coefficient();
|
||||
@@ -308,7 +309,8 @@ public class MeasurementBatchService {
|
||||
List<BigDecimal> dimensions = sortedDimensions(new BigDecimal(matcher.group(2)),
|
||||
new BigDecimal(matcher.group(3)), new BigDecimal(matcher.group(4)));
|
||||
BigDecimal coefficient = new BigDecimal(matcher.group(5));
|
||||
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient));
|
||||
BigDecimal weight = matcher.group(6) == null ? null : new BigDecimal(matcher.group(6));
|
||||
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient, weight));
|
||||
}
|
||||
tiers.sort(Comparator.comparing(SpecificationCoefficient::volume));
|
||||
return tiers;
|
||||
@@ -320,15 +322,16 @@ public class MeasurementBatchService {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient) {
|
||||
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient, BigDecimal maxWeightKg) {
|
||||
private BigDecimal volume() {
|
||||
return dimensions.get(0).multiply(dimensions.get(1)).multiply(dimensions.get(2));
|
||||
}
|
||||
|
||||
private boolean contains(List<BigDecimal> measured) {
|
||||
private boolean contains(List<BigDecimal> measured, BigDecimal unitWeightKg) {
|
||||
return measured.get(0).compareTo(dimensions.get(0)) <= 0
|
||||
&& measured.get(1).compareTo(dimensions.get(1)) <= 0
|
||||
&& measured.get(2).compareTo(dimensions.get(2)) <= 0;
|
||||
&& measured.get(2).compareTo(dimensions.get(2)) <= 0
|
||||
&& (maxWeightKg == null || unitWeightKg == null || unitWeightKg.compareTo(maxWeightKg) <= 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-9
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
public class MerchantInboundBillingService {
|
||||
@@ -55,13 +56,31 @@ public class MerchantInboundBillingService {
|
||||
platformDetail.files().stream()
|
||||
.filter(file -> "PUBLIC".equals(file.visibility()) || "MERCHANT".equals(file.visibility()))
|
||||
.toList(), List.of(), List.of());
|
||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||
FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.Detail measurement = fee.measurementId() == null || fee.measurementId() <= 0
|
||||
? measurements.detail(tenantId, inbound.getId())
|
||||
: measurements.detail(tenantId, inbound.getId(), fee.measurementId());
|
||||
var measurementItems = measurement.items().stream()
|
||||
.map(item -> new MerchantInboundBillingResponse.MeasurementItem(
|
||||
item.id(), item.itemNo(), item.boxSpecification(), item.packageCount(),
|
||||
requireOuterDimension(item.calculatedOuterLengthCm(), item.itemNo(), "长"),
|
||||
requireOuterDimension(item.calculatedOuterWidthCm(), item.itemNo(), "宽"),
|
||||
requireOuterDimension(item.calculatedOuterHeightCm(), item.itemNo(), "高"),
|
||||
item.unitVolumeCbm(), item.totalVolumeCbm(), item.volumeWeightKg(),
|
||||
item.chargeRuleId(), item.coefficientMode(), item.calculationRuleVersion()))
|
||||
.toList();
|
||||
int outerPackageCount = measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::packageCount)
|
||||
.filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
BigDecimal outerVolume = sum(measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::totalVolumeCbm).toList());
|
||||
BigDecimal outerVolumeWeight = sum(measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::volumeWeightKg).toList());
|
||||
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
||||
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
||||
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
||||
outerPackageCount, measurement.actualGrossWeightKg(),
|
||||
measurement.actualNetWeightKg(),
|
||||
measurement.actualVolumeCbm(), measurement.volumeWeightKg(), measurement.actualGoodsName());
|
||||
outerVolume, outerVolumeWeight, measurement.actualGoodsName(), "OUTER");
|
||||
var measurementImages = measurement.items().stream()
|
||||
.filter(item -> item.intelligentImageFileId() != null)
|
||||
.map(item -> new MerchantInboundBillingResponse.ImageReference(
|
||||
@@ -73,12 +92,6 @@ public class MerchantInboundBillingService {
|
||||
.map(weight -> new MerchantInboundBillingResponse.ImageReference(
|
||||
weight.fileId(), "WEIGHT", null, weight.id(), previewUrl(orderId, weight.fileId())))
|
||||
.toList();
|
||||
var measurementItems = measurement.items().stream()
|
||||
.map(item -> new MerchantInboundBillingResponse.MeasurementItem(
|
||||
item.id(), item.itemNo(), item.boxSpecification(), item.packageCount(),
|
||||
item.lengthCm(), item.widthCm(), item.heightCm(), item.unitVolumeCbm(),
|
||||
item.totalVolumeCbm(), item.volumeWeightKg()))
|
||||
.toList();
|
||||
var weightResults = measurement.weightResults().stream()
|
||||
.map(weight -> new MerchantInboundBillingResponse.WeightResult(
|
||||
weight.id(), weight.fileId(), weight.palletId(), weight.palletCode(), weight.palletType(),
|
||||
@@ -96,6 +109,14 @@ public class MerchantInboundBillingService {
|
||||
weightResults, measurementImages, weightImages, basis, fee, publishedFee(publishedFee));
|
||||
}
|
||||
|
||||
public FeeSnapshotResponse confirmFee(long tenantId, long operatorId, String operatorName,
|
||||
long orderId, int expectedFeeVersion,
|
||||
String idempotencyKey) {
|
||||
InboundOrderEntity inbound = requirePublished(tenantId, orderId);
|
||||
return pricing.confirmByCustomer(tenantId, operatorId, operatorName, inbound.getId(),
|
||||
expectedFeeVersion, idempotencyKey);
|
||||
}
|
||||
|
||||
public ImageDownload downloadImage(long tenantId, long orderId, long fileId) {
|
||||
InboundOrderEntity inbound = requirePublished(tenantId, orderId);
|
||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||
@@ -134,6 +155,19 @@ public class MerchantInboundBillingService {
|
||||
+ "/inbound-billing/images/" + fileId;
|
||||
}
|
||||
|
||||
private static BigDecimal requireOuterDimension(BigDecimal value, String itemNo, String dimension) {
|
||||
if (value == null || value.signum() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"外箱规数据不完整,明细 " + (itemNo == null ? "—" : itemNo) + " 缺少外箱" + dimension);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static BigDecimal sum(List<BigDecimal> values) {
|
||||
return values.stream().filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
private String chargeableBasis(FeeSnapshotResponse fee) {
|
||||
int compared = ("KG".equals(fee.chargeMode()) ? fee.actualWeightKg() : fee.actualVolumeCbm())
|
||||
.compareTo(fee.convertedQuantity());
|
||||
|
||||
@@ -9,8 +9,8 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL' COMMENT '订单外箱规默认计算方式:ACTUAL不调整、ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
|
||||
@@ -56,8 +56,8 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL' COMMENT '订单外箱规默认计算方式:ACTUAL不调整、ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
|
||||
@@ -26,6 +26,8 @@ CALL phase2_fba_add_column('fl_customer_charge_rule', 'height_factor', "DECIMAL(
|
||||
CALL phase2_fba_add_column('fl_customer_charge_rule', 'default_rule', "TINYINT(1) NOT NULL DEFAULT 0 COMMENT '客户默认生效规则' AFTER status");
|
||||
CALL phase2_fba_add_column('fl_carrier_fba_rate', 'version', "INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER deleted");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rule_code', "VARCHAR(50) NOT NULL DEFAULT '' COMMENT '客户规则编码快照' AFTER carrier_rate_version");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'measurement_id', "BIGINT UNSIGNED NULL COMMENT '费用绑定的测量批次ID快照' AFTER inbound_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'measurement_source', "VARCHAR(32) NULL COMMENT '计费尺寸来源快照:OUTER为外箱规数据' AFTER measurement_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_id', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '客户ID快照' AFTER rule_code");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_name', "VARCHAR(128) NOT NULL DEFAULT '' COMMENT '客户名称快照' AFTER customer_id");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'business_type', "VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型快照' AFTER customer_name");
|
||||
@@ -43,6 +45,9 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'carrier_name', "VARCHAR(128) NOT N
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'destination_code', "VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'FBA仓编码快照' AFTER carrier_name");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '价格生效开始快照' AFTER destination_code");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_by', "BIGINT UNSIGNED NULL COMMENT '客户确认人用户ID' AFTER sent_at");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_by_name', "VARCHAR(64) NULL COMMENT '客户确认人名称快照' AFTER customer_confirmed_by");
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'customer_confirmed_at', "DATETIME(3) NULL COMMENT '客户确认费用时间;非空后费用版本锁定' AFTER customer_confirmed_by_name");
|
||||
CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json");
|
||||
CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键' AFTER warehouse_id");
|
||||
|
||||
@@ -59,6 +59,12 @@ CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_width_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''外箱规宽度cm'' AFTER outer_length_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_height_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''外箱规高度cm'' AFTER outer_width_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_length_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规长度计算基数cm'' AFTER outer_height_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_width_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规宽度计算基数cm'' AFTER manual_outer_length_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'manual_outer_height_cm',
|
||||
'DECIMAL(12,3) NULL COMMENT ''仓库人工外箱规高度计算基数cm'' AFTER manual_outer_width_cm');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'charge_rule_id',
|
||||
'BIGINT UNSIGNED NULL COMMENT ''该测量明细绑定的客户计费规则ID'' AFTER volume_weight_kg');
|
||||
CALL phase2_measure_add_column('fl_inbound_measurement_item', 'coefficient_mode',
|
||||
|
||||
@@ -271,15 +271,18 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
total_volume_cbm DECIMAL(18,4) NOT NULL COMMENT '本箱规总体积m3',
|
||||
volume_weight_kg DECIMAL(18,3) NOT NULL COMMENT '本箱规材积重kg',
|
||||
charge_rule_id BIGINT UNSIGNED NULL COMMENT '该测量明细绑定的客户计费规则ID',
|
||||
coefficient_mode VARCHAR(16) NULL COMMENT '系数模式快照:ADD或MULTIPLY',
|
||||
coefficient_mode VARCHAR(16) NULL COMMENT '箱规计算策略快照:ACTUAL、ADD或MULTIPLY',
|
||||
calculation_rule_version VARCHAR(64) NOT NULL COMMENT '体积计算和舍入规则版本',
|
||||
source_image_file_id BIGINT UNSIGNED NULL COMMENT '测量设备图片文件ID',
|
||||
machine_no VARCHAR(64) NULL COMMENT '本次测量设备编号',
|
||||
device_measured_at DATETIME(3) NULL COMMENT '本次设备测量时间',
|
||||
over_limit_warning VARCHAR(500) NULL COMMENT '超线提醒快照',
|
||||
outer_length_cm DECIMAL(12,3) NULL COMMENT '外箱规长度cm',
|
||||
outer_width_cm DECIMAL(12,3) NULL COMMENT '外箱规宽度cm',
|
||||
outer_height_cm DECIMAL(12,3) NULL COMMENT '外箱规高度cm',
|
||||
outer_length_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规长度cm',
|
||||
outer_width_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规宽度cm',
|
||||
outer_height_cm DECIMAL(12,3) NULL COMMENT '策略计算后的最终外箱规高度cm',
|
||||
manual_outer_length_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规长度计算基数cm',
|
||||
manual_outer_width_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规宽度计算基数cm',
|
||||
manual_outer_height_cm DECIMAL(12,3) NULL COMMENT '仓库人工外箱规高度计算基数cm',
|
||||
remark VARCHAR(500) NULL COMMENT '测量明细备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
@@ -621,6 +624,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
measurement_id BIGINT UNSIGNED NULL COMMENT '费用绑定的测量批次ID快照',
|
||||
measurement_source VARCHAR(32) NULL COMMENT '计费尺寸来源快照:OUTER为外箱规数据',
|
||||
fee_no VARCHAR(40) NOT NULL COMMENT '客户内唯一费用单号',
|
||||
fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本',
|
||||
charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空',
|
||||
@@ -670,6 +675,9 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
confirmed_at DATETIME(3) NULL COMMENT '确认时间',
|
||||
sent_by BIGINT UNSIGNED NULL COMMENT '发送人ID',
|
||||
sent_at DATETIME(3) NULL COMMENT '发送客户时间',
|
||||
customer_confirmed_by BIGINT UNSIGNED NULL COMMENT '客户确认人用户ID',
|
||||
customer_confirmed_by_name VARCHAR(64) NULL COMMENT '客户确认人名称快照',
|
||||
customer_confirmed_at DATETIME(3) NULL COMMENT '客户确认费用时间;非空后费用版本锁定',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
@@ -862,8 +870,8 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '长宽高各自增加一次的厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD' COMMENT '订单外箱规默认计算方式:ADD或MULTIPLY',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL' COMMENT '订单外箱规默认计算方式:ACTUAL不调整、ADD或MULTIPLY',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
|
||||
@@ -341,6 +341,10 @@ components:
|
||||
confirmedAt: {type: string, format: date-time, nullable: true}
|
||||
sentAt: {type: string, format: date-time, nullable: true}
|
||||
nextProcessStatus: {type: string, nullable: true, description: 费用动作后的入库主流程状态;审核完成后保持 WAIT_SPLIT}
|
||||
customerConfirmationStatus: {type: string, enum: [PENDING_CONFIRMATION, CONFIRMED], description: 客户对当前费用版本的确认状态}
|
||||
customerConfirmedBy: {type: string, nullable: true, description: 客户确认人用户ID}
|
||||
customerConfirmedByName: {type: string, nullable: true, description: 客户确认人名称快照}
|
||||
customerConfirmedAt: {type: string, format: date-time, nullable: true, description: 客户确认时间;非空后当前版本不可修改}
|
||||
InboundFeeDetailSaveRequest:
|
||||
type: object
|
||||
required: [feeItemId, direction, businessDate, unitPrice, currency, amount]
|
||||
|
||||
@@ -1044,7 +1044,7 @@ paths:
|
||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||
get:
|
||||
summary: 客户查询已完成入库结果和最新已发布费用
|
||||
description: 需要 merchant:order:view 权限;客户仅能按登录态客户范围只读查看。入库流程达到 WAIT_SPLIT 后,费用在 LOADING、LOADED、DELIVERED 仍持续可见;始终要求 measureReviewStatus=APPROVED、feeStatus=SENT 且存在 PUBLISHED 入库运费明细,不需要客户确认费用。CONFIRMED 只表示发送前确认完成,尚未生成可对客发布明细。
|
||||
description: 需要 merchant:order:view 权限;客户仅能按登录态客户范围查看。入库流程达到 WAIT_SPLIT 后,费用在 LOADING、LOADED、DELIVERED 仍持续可见;始终要求 measureReviewStatus=APPROVED、feeStatus=SENT 且存在 PUBLISHED 入库运费明细。latestFee.customerConfirmationStatus 表示客户是否已确认当前费用版本。
|
||||
parameters:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
@@ -1054,6 +1054,26 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/MerchantInboundBillingEnvelope'}
|
||||
'403': {description: 非当前客户或无订单查看权限}
|
||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing/confirm:
|
||||
post:
|
||||
summary: 客户确认已发布的入库费用
|
||||
description: 需要 merchant:order:edit 权限;仅可确认当前客户 SENT 状态的最新费用版本。确认成功后记录当前成员与确认时间,并永久锁定该费用版本,平台不得再修改。
|
||||
parameters:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {$ref: '#/components/parameters/IdempotencyKey'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [expectedFeeVersion]
|
||||
properties:
|
||||
expectedFeeVersion: {type: integer, minimum: 1}
|
||||
responses:
|
||||
'200': {description: 成功时返回 customerConfirmationStatus=CONFIRMED 的当前费用快照}
|
||||
'403': {description: 非当前客户或无订单编辑权限}
|
||||
'409': {description: 费用未发送、版本已变化或已经确认}
|
||||
/api/platform/v1/loading-plans/{loadingPlanId}/fee-details/{feeId}/adjust:
|
||||
post:
|
||||
summary: 调整已审核柜次费用的日期和金额
|
||||
@@ -1429,7 +1449,7 @@ components:
|
||||
type: object
|
||||
required: [defaultCalculationMode]
|
||||
properties:
|
||||
defaultCalculationMode: {type: string, enum: [ADD, MULTIPLY]}
|
||||
defaultCalculationMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY]}
|
||||
CustomerDimensionConfigRuleUpdateRequest:
|
||||
type: object
|
||||
required: [boxType, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm, version]
|
||||
@@ -1443,7 +1463,7 @@ components:
|
||||
minimum: 0.001
|
||||
maximum: 100
|
||||
multipleOf: 0.001
|
||||
description: 箱规长、宽、高三个方向分别增加一次的厘米数
|
||||
description: 箱规长、宽、高三个方向分别使用的每侧加宽厘米数;每个完整尺寸增加该值的2倍
|
||||
example: 2.000
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
version: {type: integer, minimum: 0, description: 乐观锁版本号}
|
||||
@@ -1452,7 +1472,7 @@ components:
|
||||
required: [tenantId, defaultCalculationMode, rules]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
defaultCalculationMode: {type: string, enum: [ADD, MULTIPLY]}
|
||||
defaultCalculationMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY]}
|
||||
rules:
|
||||
type: array
|
||||
minItems: 4
|
||||
@@ -1469,9 +1489,9 @@ components:
|
||||
innerWidthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerHeightCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
edgeAdditionCm: {type: number, minimum: 0.001, maximum: 100, example: 2.000}
|
||||
outerLengthCm: {type: number, minimum: 0.001, description: innerLengthCm + edgeAdditionCm}
|
||||
outerWidthCm: {type: number, minimum: 0.001, description: innerWidthCm + edgeAdditionCm}
|
||||
outerHeightCm: {type: number, minimum: 0.001, description: innerHeightCm + edgeAdditionCm}
|
||||
outerLengthCm: {type: number, minimum: 0.001, description: innerLengthCm + edgeAdditionCm * 2}
|
||||
outerWidthCm: {type: number, minimum: 0.001, description: innerWidthCm + edgeAdditionCm * 2}
|
||||
outerHeightCm: {type: number, minimum: 0.001, description: innerHeightCm + edgeAdditionCm * 2}
|
||||
volumeCbm: {type: number, multipleOf: 0.001, description: 使用未舍入外径完整计算后四舍五入保留3位}
|
||||
remark: {type: string, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
@@ -1585,8 +1605,12 @@ components:
|
||||
outerLengthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱长度;未保存人工修正时为空}
|
||||
outerWidthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱宽度;未保存人工修正时为空}
|
||||
outerHeightCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱高度;未保存人工修正时为空}
|
||||
calculatedOuterLengthCm: {type: number, format: double, nullable: true, description: 已落库的最终外箱规长度}
|
||||
calculatedOuterWidthCm: {type: number, format: double, nullable: true, description: 已落库的最终外箱规宽度}
|
||||
calculatedOuterHeightCm: {type: number, format: double, nullable: true, description: 已落库的最终外箱规高度}
|
||||
calculationRuleVersion: {type: string, nullable: true, description: 当前外箱规所绑定的箱规或系数规则版本}
|
||||
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true}
|
||||
coefficientMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY], nullable: true}
|
||||
remark: {type: string, nullable: true}
|
||||
boxCode: {type: string, nullable: true}
|
||||
scannedShipmentId: {type: string, nullable: true}
|
||||
@@ -1696,13 +1720,14 @@ components:
|
||||
lengthCm: {type: number, minimum: 0.001}
|
||||
widthCm: {type: number, minimum: 0.001}
|
||||
heightCm: {type: number, minimum: 0.001}
|
||||
unitWeightKg: {type: number, minimum: 0, multipleOf: 0.0001, nullable: true, description: 实际总毛重按总箱数均分后的单箱毛重;用于系数挡位匹配}
|
||||
intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
overLimitWarning: {type: string, maxLength: 500, nullable: true}
|
||||
outerLengthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱长度修正;保存后参与联动计算}
|
||||
outerWidthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱宽度修正;保存后参与联动计算}
|
||||
outerHeightCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱高度修正;保存后参与联动计算}
|
||||
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true}
|
||||
coefficientMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY], nullable: true}
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
InboundMeasurementDetailEnvelope:
|
||||
type: object
|
||||
|
||||
+8
-3
@@ -35,14 +35,19 @@ class MerchantInboundBillingControllerContractTest {
|
||||
.isEqualTo("hasAuthority('merchant:order:view')");
|
||||
assertThat(method.getAnnotation(Operation.class).description())
|
||||
.contains("WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED", "APPROVED + SENT", "已发布入库运费明细");
|
||||
assertThat(MerchantInboundBillingController.class.getDeclaredMethods())
|
||||
.noneMatch(candidate -> candidate.getName().toLowerCase().contains("confirm"));
|
||||
var confirm = MerchantInboundBillingController.class.getDeclaredMethod(
|
||||
"confirm", AuthenticatedUser.class, long.class, String.class,
|
||||
com.baiyue.firstleg.dto.MerchantInboundBillingResponse.ConfirmFeeRequest.class);
|
||||
assertThat(confirm.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('merchant:order:edit')");
|
||||
|
||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
||||
.contains("WAIT_SPLIT", "LOADING、LOADED、DELIVERED", "measureReviewStatus=APPROVED",
|
||||
"feeStatus=SENT", "PUBLISHED 入库运费明细", "CONFIRMED 只表示发送前确认完成");
|
||||
"feeStatus=SENT", "PUBLISHED 入库运费明细", "customerConfirmationStatus")
|
||||
.contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing/confirm:",
|
||||
"确认成功后记录当前成员与确认时间", "平台不得再修改");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+12
-9
@@ -90,10 +90,10 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.scannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1680");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("62.000");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1010");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("63.500");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("45.200");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("35.300");
|
||||
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_THIRD_PARTY);
|
||||
assertThat(measurement.getSourceCode()).isEqualTo("DEVICE");
|
||||
}
|
||||
@@ -155,16 +155,16 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.created()).isFalse();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(4);
|
||||
assertThat(existing.getLengthCm()).isEqualByComparingTo("59.500");
|
||||
assertThat(existing.getUnitVolumeCbm()).isEqualByComparingTo("0.168");
|
||||
assertThat(existing.getTotalVolumeCbm()).isEqualByComparingTo("0.6720");
|
||||
assertThat(existing.getVolumeWeightKg()).isEqualByComparingTo("112.224");
|
||||
assertThat(existing.getUnitVolumeCbm()).isEqualByComparingTo("0.101");
|
||||
assertThat(existing.getTotalVolumeCbm()).isEqualByComparingTo("0.4040");
|
||||
assertThat(existing.getVolumeWeightKg()).isEqualByComparingTo("67.468");
|
||||
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(existing.getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanOutsideConfiguredWeightingRulesKeepsImageAndActualDimensions() {
|
||||
void scanOutsideConfiguredWeightingRulesKeepsImageAndUsesLargestTierAddition() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
|
||||
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
|
||||
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
|
||||
@@ -182,8 +182,11 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.created()).isTrue();
|
||||
assertThat(insertedItem.get().getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(insertedItem.get().getLengthCm()).isEqualByComparingTo("60.100");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.100");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("54.100");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("54.100");
|
||||
assertThat(insertedItem.get().getCalculationRuleVersion())
|
||||
.isEqualTo("ACTUAL_DIMENSION_V1");
|
||||
.isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V0");
|
||||
assertThat(insertedItem.get().getOverLimitWarning()).isNull();
|
||||
verify(images, never()).delete(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE));
|
||||
}
|
||||
|
||||
+4
-4
@@ -95,10 +95,10 @@ class CustomerDimensionConfigServiceTest {
|
||||
assertThat(result.rules()).extracting(rule -> rule.boxType())
|
||||
.containsExactly(CustomerDimensionBoxType.values());
|
||||
assertThat(result.rules().get(0).boxTypeName()).isEqualTo("小箱");
|
||||
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("32.000");
|
||||
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.015");
|
||||
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.020");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class CustomerDimensionPricingFlowTest {
|
||||
var kgEstimate = pricing.estimate(rate("KG", "8.700000"), actualWeight, twoCartonsVolume);
|
||||
var cbmEstimate = pricing.estimate(rate("CBM", "1400.000000"), actualWeight, twoCartonsVolume);
|
||||
|
||||
assertThat(twoCartonsVolume).isEqualByComparingTo("0.030");
|
||||
assertThat(twoCartonsVolume).isEqualByComparingTo("0.006");
|
||||
assertThat(kgEstimate.quantity()).isEqualByComparingTo("24.000000");
|
||||
assertThat(kgEstimate.amount()).isEqualByComparingTo("208.80");
|
||||
assertThat(cbmEstimate.quantity()).isEqualByComparingTo("0.066116");
|
||||
|
||||
+10
-10
@@ -26,16 +26,16 @@ class CustomerDimensionWeightingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void measuredDimensionsUseSmallestContainingRuleAndAddConfiguredValueOncePerDimension() {
|
||||
void measuredDimensionsUseSmallestContainingRuleAndAddEachSide() {
|
||||
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("32.000");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.015488");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.015");
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("14.000");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("14.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("14.000");
|
||||
assertThat(weighted.rawVolumeCbm()).isEqualByComparingTo("0.002744");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.003");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,7 +52,7 @@ class CustomerDimensionWeightingServiceTest {
|
||||
new BigDecimal("30.001"), new BigDecimal("20"), new BigDecimal("20"));
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.MEDIUM);
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.043");
|
||||
assertThat(weighted.unitVolumeCbm()).isEqualByComparingTo("0.020");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,9 +62,9 @@ class CustomerDimensionWeightingServiceTest {
|
||||
|
||||
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.EXTRA_LARGE);
|
||||
assertThat(weighted.edgeAdditionCm()).isEqualByComparingTo("2.000");
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("62.001");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("64.001");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(weighted.calculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V3");
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -24,13 +24,13 @@ class CustomerDimensionWeightingTerminalRulesTest {
|
||||
var ruleSet = service.load(10001L);
|
||||
|
||||
assertWeighted(ruleSet, "10", "10", "10", CustomerDimensionBoxType.SMALL,
|
||||
"32", "22", "22", "0.015");
|
||||
"14", "14", "14", "0.003");
|
||||
assertWeighted(ruleSet, "35", "25", "25", CustomerDimensionBoxType.MEDIUM,
|
||||
"42", "32", "32", "0.043");
|
||||
"39", "29", "29", "0.033");
|
||||
assertWeighted(ruleSet, "45", "35", "35", CustomerDimensionBoxType.LARGE,
|
||||
"52", "42", "42", "0.092");
|
||||
"49", "39", "39", "0.075");
|
||||
assertWeighted(ruleSet, "55", "45", "45", CustomerDimensionBoxType.EXTRA_LARGE,
|
||||
"62", "52", "52", "0.168");
|
||||
"59", "49", "49", "0.142");
|
||||
}
|
||||
|
||||
private static void assertWeighted(CustomerDimensionWeightingService.RuleSet rules,
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
|
||||
@@ -27,6 +29,7 @@ import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -49,6 +52,7 @@ class FbaPricingServiceTest {
|
||||
private static final long INBOUND = 30001L;
|
||||
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
|
||||
@MockBean private FbaPricingRepository repository;
|
||||
@MockBean private com.baiyue.firstleg.repository.InboundMeasurementRepository measurementRepository;
|
||||
@MockBean private InboundFeeDetailService feeDetails;
|
||||
@MockBean private ActivePriceBookEstimator priceBookEstimator;
|
||||
@Resource private FbaPricingService service;
|
||||
@@ -100,6 +104,35 @@ class FbaPricingServiceTest {
|
||||
assertThat(fee.getValue().getRoundingMode()).isEqualTo("HALF_UP");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("外箱规计费保存测量批次和尺寸来源快照")
|
||||
void bindsSavedOuterMeasurementWhenCalculatingCustomerFee() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("62.500", "1.0019");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(301L);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setOuterLengthCm(new BigDecimal("34"));
|
||||
item.setOuterWidthCm(new BigDecimal("24"));
|
||||
item.setOuterHeightCm(new BigDecimal("64"));
|
||||
item.setCalculationRuleVersion("CUSTOMER_DIMENSION_SMALL_V3");
|
||||
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(any())).thenReturn(
|
||||
estimate("CBM", "1.0019", "100", "100.19", "0"));
|
||||
|
||||
service.calculate(TENANT, OPERATOR, INBOUND, NOW,
|
||||
new BigDecimal("62.500"), new BigDecimal("1.0019"),
|
||||
"OUTER", "OUTER", "calc-outer");
|
||||
|
||||
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
|
||||
verify(repository).insertFee(fee.capture());
|
||||
assertThat(fee.getValue().getMeasurementId()).isEqualTo(301L);
|
||||
assertThat(fee.getValue().getMeasurementSource()).isEqualTo("OUTER");
|
||||
assertThat(fee.getValue().getActualVolumeCbm()).isEqualByComparingTo("1.0019");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缺少订单客户归属时拒绝生成对客费用")
|
||||
void rejectsMissingOrderCustomerOwnership() {
|
||||
@@ -182,44 +215,31 @@ class FbaPricingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("装柜完成后的自动费用仍可确认")
|
||||
void confirmsCalculatedFeeAfterLoadingCompleted() {
|
||||
@DisplayName("装柜完成后禁止确认费用")
|
||||
void rejectsConfirmAfterLoadingCompleted() {
|
||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
inbound.setFeeStatus("CALCULATED");
|
||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CONFIRM", "confirm-loaded"))
|
||||
.thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L);
|
||||
return null;
|
||||
}).when(repository).insertIdempotency(any());
|
||||
when(repository.confirmFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundConfirmed(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(fee);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
|
||||
FeeSnapshotResponse response = service.confirm(
|
||||
TENANT, OPERATOR, INBOUND, "confirm-loaded");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("LOADED");
|
||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||
assertThatThrownBy(() -> service.confirm(TENANT, OPERATOR, INBOUND, "confirm-loaded"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("装柜流程");
|
||||
verify(repository, never()).markInboundConfirmed(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核未通过的测量快照禁止计费")
|
||||
void rejectsUnapprovedMeasurementSnapshot() {
|
||||
@DisplayName("审核未通过的测量快照也允许计费")
|
||||
void allowsUnapprovedMeasurementSnapshot() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setMeasureReviewStatus("REJECTED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
estimate("CBM", "1.0000", "10", "10", "0"));
|
||||
|
||||
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("审核通过");
|
||||
verify(priceBookEstimator, never()).estimate(any());
|
||||
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3")).isNotNull();
|
||||
verify(priceBookEstimator).estimate(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -368,6 +388,55 @@ class FbaPricingServiceTest {
|
||||
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户确认已发送费用后记录确认人并锁定版本")
|
||||
void customerConfirmsSentFeeAndLocksVersion() {
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "CUSTOMER_FEE_CONFIRM", "customer-confirm-1"))
|
||||
.thenReturn(null);
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(910L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.confirmFeeByCustomer(TENANT, 501L, 1, OPERATOR, "客户成员", NOW))
|
||||
.thenAnswer(invocation -> {
|
||||
current.setCustomerConfirmedBy(OPERATOR);
|
||||
current.setCustomerConfirmedByName("客户成员");
|
||||
current.setCustomerConfirmedAt(NOW);
|
||||
return 1;
|
||||
});
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(current);
|
||||
|
||||
FeeSnapshotResponse response = service.confirmByCustomer(
|
||||
TENANT, OPERATOR, "客户成员", INBOUND, 1, "customer-confirm-1");
|
||||
|
||||
assertThat(response.customerConfirmationStatus()).isEqualTo("CONFIRMED");
|
||||
assertThat(response.customerConfirmedBy()).isEqualTo(OPERATOR);
|
||||
assertThat(response.customerConfirmedByName()).isEqualTo("客户成员");
|
||||
assertThat(response.customerConfirmedAt()).isEqualTo(NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已确认费用时拒绝平台再次修改")
|
||||
void rejectsAdjustmentAfterCustomerConfirmation() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
current.setCustomerConfirmedAt(NOW);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
|
||||
|
||||
assertThatThrownBy(() -> service.adjustPublished(TENANT, OPERATOR, INBOUND,
|
||||
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
|
||||
1, new BigDecimal("12.5"), new BigDecimal("8.00"),
|
||||
BigDecimal.ZERO, "客服修正"), "locked-adjust"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("客户已确认");
|
||||
|
||||
verify(repository, never()).insertIdempotency(any());
|
||||
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("初算费用调整后保持初算状态且不发布财务来源")
|
||||
void adjustsCalculatedFeeWithoutPublishingIt() {
|
||||
|
||||
+10
-1
@@ -41,6 +41,8 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -235,6 +237,9 @@ class InboundMeasurementServiceTest {
|
||||
item.setOuterLengthCm(new BigDecimal("65.000"));
|
||||
item.setOuterWidthCm(new BigDecimal("55.000"));
|
||||
item.setOuterHeightCm(new BigDecimal("55.000"));
|
||||
item.setManualOuterLengthCm(new BigDecimal("65.000"));
|
||||
item.setManualOuterWidthCm(new BigDecimal("55.000"));
|
||||
item.setManualOuterHeightCm(new BigDecimal("55.000"));
|
||||
item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_V1");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
|
||||
@@ -260,6 +265,7 @@ class InboundMeasurementServiceTest {
|
||||
item.setOuterLengthCm(new BigDecimal("65.000"));
|
||||
item.setOuterWidthCm(new BigDecimal("54.000"));
|
||||
item.setOuterHeightCm(new BigDecimal("54.000"));
|
||||
item.setManualOuterLengthCm(new BigDecimal("65.000"));
|
||||
item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_L_V1");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
|
||||
@@ -894,7 +900,8 @@ class InboundMeasurementServiceTest {
|
||||
|
||||
@Test
|
||||
void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundOrderEntity inbound = spy(inbound("MEASURING"));
|
||||
clearInvocations(inbound);
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
measurement.setTenantId(1L);
|
||||
@@ -928,6 +935,8 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(inbound.getActualNetWeightKg()).isEqualByComparingTo("20.000");
|
||||
assertThat(inbound.getPalletCount()).isEqualTo(1);
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("1.2345");
|
||||
assertThat(inbound.getTotalPackageCount()).isEqualTo(3);
|
||||
verify(inbound, never()).setTotalPackageCount(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+61
-26
@@ -212,12 +212,12 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("61.500");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("43.200");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("33.300");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("214.500");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.088");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.1760");
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("63.500");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("45.200");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("35.300");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("224.500");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.101");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.2020");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -235,12 +235,12 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("62.001");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("52.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("270.001");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.168");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.3360");
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("64.001");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("280.001");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.187");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.3740");
|
||||
assertThat(calculated.getCalculationRuleVersion())
|
||||
.isEqualTo("CUSTOMER_DIMENSION_EXTRA_LARGE_V0");
|
||||
}
|
||||
@@ -261,13 +261,16 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("50.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("40.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("30.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("200.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0699");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.1398");
|
||||
assertThat(calculated.getVolumeWeightKg()).isEqualByComparingTo("23.347");
|
||||
assertThat(calculated.getManualOuterLengthCm()).isEqualByComparingTo("50");
|
||||
assertThat(calculated.getManualOuterWidthCm()).isEqualByComparingTo("40");
|
||||
assertThat(calculated.getManualOuterHeightCm()).isEqualByComparingTo("30");
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("44.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("210.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0808");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.1616");
|
||||
assertThat(calculated.getVolumeWeightKg()).isEqualByComparingTo("26.988");
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_V1");
|
||||
}
|
||||
|
||||
@@ -287,15 +290,44 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("50.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("32.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("22.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0367");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.0734");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("160.000");
|
||||
assertThat(calculated.getManualOuterLengthCm()).isEqualByComparingTo("50");
|
||||
assertThat(calculated.getManualOuterWidthCm()).isNull();
|
||||
assertThat(calculated.getManualOuterHeightCm()).isNull();
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0441");
|
||||
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("0.0882");
|
||||
assertThat(calculated.getGirthCm()).isEqualByComparingTo("170.000");
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_L_V1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualStrategyUsesEachWarehouseValueAndFillsBlankDimensionsFromMeasurement() {
|
||||
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
|
||||
80001L, "001", "A", 1, new BigDecimal("61.1"), new BigDecimal("41.2"),
|
||||
new BigDecimal("31.3"), 500L, null, new BigDecimal("11"),
|
||||
null, new BigDecimal("11"), null, "ACTUAL", "宽未填写");
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
1L, 10L, null, 0, "MANUAL", "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, null, null, null, null, 9L, List.of(source));
|
||||
|
||||
service.saveBatch(command);
|
||||
|
||||
ArgumentCaptor<List<InboundMeasurementItemEntity>> items = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getManualOuterLengthCm()).isEqualByComparingTo("11");
|
||||
assertThat(calculated.getManualOuterWidthCm()).isNull();
|
||||
assertThat(calculated.getManualOuterHeightCm()).isEqualByComparingTo("11");
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("11.000");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("41.200");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("11.000");
|
||||
assertThat(calculated.getCoefficientMode()).isEqualTo("ACTUAL");
|
||||
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_LH_V1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseOverrideUsesTheTierMatchedByItsBaseDimensionsBeforeMultiplyStrategy() {
|
||||
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
|
||||
@@ -325,7 +357,10 @@ class MeasurementBatchServiceTest {
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), items.capture());
|
||||
InboundMeasurementItemEntity calculated = items.getValue().get(0);
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("31.000");
|
||||
assertThat(calculated.getManualOuterLengthCm()).isEqualByComparingTo("31");
|
||||
assertThat(calculated.getManualOuterWidthCm()).isNull();
|
||||
assertThat(calculated.getManualOuterHeightCm()).isNull();
|
||||
assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("37.200");
|
||||
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0215");
|
||||
|
||||
+53
@@ -154,6 +154,59 @@ class MerchantInboundBillingServiceTest {
|
||||
verify(pricing, never()).latest(10001L, 88L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPublishSavedOuterDimensionsAndOuterTotalsInsteadOfMeasuredDimensions() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
||||
InboundOrderDetail detail = new InboundOrderDetail(
|
||||
mock(com.baiyue.firstleg.dto.InboundOrderSummary.class), null,
|
||||
List.of(), List.of(), List.of(), List.of());
|
||||
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
|
||||
InboundMeasurementResponses.Item item = mock(InboundMeasurementResponses.Item.class);
|
||||
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
|
||||
InboundFeeDetailEntity published = new InboundFeeDetailEntity();
|
||||
published.setId(601L);
|
||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||
when(orders.detail(10001L, inbound)).thenReturn(detail);
|
||||
when(measurements.detail(10001L, 88L)).thenReturn(measurement);
|
||||
when(measurement.items()).thenReturn(List.of(item));
|
||||
when(measurement.weightResults()).thenReturn(List.of());
|
||||
when(measurement.actualGrossWeightKg()).thenReturn(new BigDecimal("62.5"));
|
||||
when(item.itemNo()).thenReturn("1");
|
||||
when(item.packageCount()).thenReturn(2);
|
||||
when(item.lengthCm()).thenReturn(new BigDecimal("30"));
|
||||
when(item.widthCm()).thenReturn(new BigDecimal("20"));
|
||||
when(item.heightCm()).thenReturn(new BigDecimal("60"));
|
||||
when(item.calculatedOuterLengthCm()).thenReturn(new BigDecimal("34"));
|
||||
when(item.calculatedOuterWidthCm()).thenReturn(new BigDecimal("24"));
|
||||
when(item.calculatedOuterHeightCm()).thenReturn(new BigDecimal("64"));
|
||||
when(item.totalVolumeCbm()).thenReturn(new BigDecimal("0.1046"));
|
||||
when(item.volumeWeightKg()).thenReturn(new BigDecimal("17.469"));
|
||||
when(item.coefficientMode()).thenReturn("ADD");
|
||||
when(item.calculationRuleVersion()).thenReturn("CUSTOMER_DIMENSION_SMALL_V3");
|
||||
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||
when(feeDetails.publishedFreight(10001L, 88L)).thenReturn(published);
|
||||
when(fee.chargeMode()).thenReturn("CBM");
|
||||
when(fee.measurementId()).thenReturn(301L);
|
||||
when(fee.chargeUnit()).thenReturn("CBM");
|
||||
when(fee.actualVolumeCbm()).thenReturn(new BigDecimal("0.1046"));
|
||||
when(fee.convertedQuantity()).thenReturn(new BigDecimal("0.1000"));
|
||||
when(measurements.detail(10001L, 88L, 301L)).thenReturn(measurement);
|
||||
|
||||
var result = service.find(10001L, 77L);
|
||||
|
||||
assertThat(result.measurementItems()).singleElement().satisfies(value -> {
|
||||
assertThat(value.lengthCm()).isEqualByComparingTo("34");
|
||||
assertThat(value.widthCm()).isEqualByComparingTo("24");
|
||||
assertThat(value.heightCm()).isEqualByComparingTo("64");
|
||||
assertThat(value.calculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_SMALL_V3");
|
||||
});
|
||||
assertThat(result.measurementSummary().actualPackageCount()).isEqualTo(2);
|
||||
assertThat(result.measurementSummary().actualVolumeCbm()).isEqualByComparingTo("0.1046");
|
||||
assertThat(result.measurementSummary().volumeWeightKg()).isEqualByComparingTo("17.469");
|
||||
assertThat(result.measurementSummary().calculationSource()).isEqualTo("OUTER");
|
||||
verify(measurements).detail(10001L, 88L, 301L);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"FEE_CONFIRMING", "VOIDED"})
|
||||
void shouldRejectProcessStatusesOutsidePublishedFulfillmentLifecycle(String processStatus) {
|
||||
|
||||
@@ -306,7 +306,7 @@ public class UnifiedOrderQueryService {
|
||||
order.getCustomerId(), order.getCustomerName(),
|
||||
order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
|
||||
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
|
||||
order.getExpectedShipDate(), inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(),
|
||||
order.getExpectedShipDate(), displayedPackageCount(order, cargo, inbound), null, inbound.locationCodes(),
|
||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
|
||||
@@ -333,7 +333,7 @@ public class UnifiedOrderQueryService {
|
||||
order != null && Boolean.TRUE.equals(order.getChannelChanged()), inbound.warehouseId(),
|
||||
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(),
|
||||
inbound.id(), inbound.version(), inbound.inboundCode(), null,
|
||||
inbound.totalPackageCount() != null ? inbound.totalPackageCount() : totalPackageCount(cargo), null, inbound.locationCodes(),
|
||||
displayedPackageCount(order, cargo, inbound), null, inbound.locationCodes(),
|
||||
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
|
||||
@@ -416,6 +416,15 @@ public class UnifiedOrderQueryService {
|
||||
return cargo == null ? null : cargo.totalPackageCount();
|
||||
}
|
||||
|
||||
private Integer displayedPackageCount(OrderEntity order, CargoSummary cargo,
|
||||
FirstlegUnifiedOrderClient.Inbound inbound) {
|
||||
Integer declaredPackageCount = totalPackageCount(cargo);
|
||||
boolean splitChild = order != null
|
||||
&& (order.getParentOrderId() != null || "CHILD".equals(order.getOrderRole()));
|
||||
if (!splitChild && declaredPackageCount != null) return declaredPackageCount;
|
||||
return inbound.totalPackageCount() != null ? inbound.totalPackageCount() : declaredPackageCount;
|
||||
}
|
||||
|
||||
private BigDecimal grossWeightKg(CargoSummary cargo) {
|
||||
return cargo == null ? null : cargo.grossWeightKg();
|
||||
}
|
||||
|
||||
@@ -167,6 +167,25 @@ class UnifiedOrderQueryServiceTest {
|
||||
.extracting(UnifiedOrderResponse.Row::totalPackageCount).isEqualTo(35);
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularOrderAlwaysDisplaysDeclaredCargoCountInsteadOfMeasuredInboundSnapshot() {
|
||||
UnifiedOrderQuery query = query(null);
|
||||
OrderEntity order = order(OrderStatus.APPROVED);
|
||||
OrderCargoEntity declaredCargo = new OrderCargoEntity();
|
||||
declaredCargo.setOrderId(77L);
|
||||
declaredCargo.setPackageCount(100);
|
||||
var summary = summary("WAIT_SPLIT", null);
|
||||
when(summary.inbound().totalPackageCount()).thenReturn(3);
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), any()))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
when(firstleg.summaries(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(List.of(summary)));
|
||||
when(orders.findCargoByOrderIds(10001L, List.of(77L))).thenReturn(List.of(declaredCargo));
|
||||
|
||||
assertThat(service.page(10001L, query).records()).singleElement()
|
||||
.extracting(UnifiedOrderResponse.Row::totalPackageCount).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundMasterUsesTotalPackageCountEvenWhenLegacyPackageCountIsAbsent() {
|
||||
UnifiedOrderQuery query = query(null);
|
||||
|
||||
Reference in New Issue
Block a user