修复(计费): 升级报价拒绝越界数值入参
- 新增 RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput 及 wire token - 报价前校验期号范围与账期窗口,避免 saturating_sub 静默夹取 - 新增 invalid_numeric_input_is_rejected 回归测试 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -67,6 +67,8 @@ pub enum RuntimeProfileMembershipUpgradeRejection {
|
||||
NotUpgrade,
|
||||
/// 目标档位不可购买(非会员占位或已下架)。
|
||||
TargetPlanNotPurchasable,
|
||||
/// 报价入参的数值不变量被破坏(期号越界或账期窗口非法):拒绝,而不是静默夹取。
|
||||
InvalidQuoteInput,
|
||||
}
|
||||
|
||||
impl RuntimeProfileMembershipUpgradeRejection {
|
||||
@@ -76,6 +78,7 @@ impl RuntimeProfileMembershipUpgradeRejection {
|
||||
Self::CycleKindChangeUnsupported => "cycle_kind_change_unsupported",
|
||||
Self::NotUpgrade => "not_upgrade",
|
||||
Self::TargetPlanNotPurchasable => "target_plan_not_purchasable",
|
||||
Self::InvalidQuoteInput => "invalid_quote_input",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,10 +119,28 @@ pub fn check_runtime_profile_membership_upgrade_allowed(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 报价入参的数值自检。生产入口从会员行构造入参,这些不变量恒成立;
|
||||
/// 在此显式校验是为了不让越界值被 `saturating_sub` 静默夹取后算出错误金额。
|
||||
///
|
||||
/// 刻意不做的两项(见 review 第 12 项):`cycle_count` 与周期类型的对齐(月付 1 / 年付 12)、
|
||||
/// `now` 的窗口上界——已过期报价按「剩余比例为 0」返回,属既有契约。
|
||||
fn validate_runtime_profile_membership_upgrade_quote_input(
|
||||
input: &RuntimeProfileMembershipUpgradeQuoteInput,
|
||||
) -> Result<(), RuntimeProfileMembershipUpgradeRejection> {
|
||||
let cycle_index_out_of_range = input.cycle_index == 0 || input.cycle_index > input.cycle_count;
|
||||
let window_out_of_order = input.cycle_resets_at_micros < input.cycle_started_at_micros;
|
||||
let now_before_window = input.now_micros < input.cycle_started_at_micros;
|
||||
if cycle_index_out_of_range || window_out_of_order || now_before_window {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 计算补差升级报价;报价与下单重算必须共用本函数。
|
||||
pub fn quote_runtime_profile_membership_upgrade(
|
||||
input: RuntimeProfileMembershipUpgradeQuoteInput,
|
||||
) -> Result<RuntimeProfileMembershipUpgradeQuote, RuntimeProfileMembershipUpgradeRejection> {
|
||||
validate_runtime_profile_membership_upgrade_quote_input(&input)?;
|
||||
check_runtime_profile_membership_upgrade_allowed(&input.current, &input.target)?;
|
||||
|
||||
let current = input.current;
|
||||
@@ -344,6 +365,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_input_is_rejected() {
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
cycle_index: 0,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
);
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
cycle_index: base_input().cycle_count + 1,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
);
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
cycle_resets_at_micros: beijing(2026, 6, 1, 9),
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
);
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
now_micros: beijing(2026, 6, 1, 9),
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_kind_change_is_rejected() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user