领域层:新增会员补差升级报价纯函数
- 新增 membership_upgrade.rs:quote_runtime_profile_membership_upgrade 与 RuntimeProfileMembershipUpgradeQuoteInput / RuntimeProfileMembershipUpgradeQuote - 月付按「新月价 − 当前月价」「新每期额度 − 当前每期额度」;年付按 (新年价 − 当前年价)/12×(后续完整月数 + 本期剩余比例) - check_runtime_profile_membership_upgrade_allowed 实现允许操作矩阵:非会员 / 周期变更 / 同档降档 / 目标不可购买一律拒绝 - membership_cycle_remaining_ratio_ppm 计算本期剩余比例(ppm,向下取整) - 金额向上取到分、补点向下取整;有效期 / 刷新日 / 期数保持不变 - 单测复现 spec 示例:Plus 月→Pro 月 ¥200 / 2500 点 / 余额 3115;Plus 年→Pro 年 ¥1083.34 / 1250 点 / 余额 1865 - 设计文档同步报价函数签名与「后续完整月数 = cycle_count − cycle_index」口径 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -269,8 +269,14 @@ pub struct RuntimeProfileMembershipUpgradeQuote {
|
||||
`advance_beijing_months_clamped(anchor, months)`、`beijing_local_date_parts(micros) -> (年月日, 当日微秒)`、
|
||||
`beijing_local_micros(年月日, 当日微秒)`、`membership_expires_at(anchor, cycle_kind)`、
|
||||
`membership_cycle_remaining_micros(resets_at, now)`。
|
||||
- `quote_membership_upgrade(current, target, now) -> quote`:
|
||||
金额向上取到分、补点向下取整;月付 / 年付分支见 §6;年价取目录 `year_price_cents`。
|
||||
- `quote_runtime_profile_membership_upgrade(input) -> Result<quote, rejection>`:
|
||||
入参 `RuntimeProfileMembershipUpgradeQuoteInput`(当前档位 / 周期、目标档位 / 周期、期序号 / 期总数、
|
||||
本期窗口、当前月度余额、`now`);金额向上取到分、补点向下取整;月付 / 年付分支见 §6;年价取目录 `year_price_cents`。
|
||||
- `check_runtime_profile_membership_upgrade_allowed(...) -> Result<(), rejection>`:
|
||||
§3.4 允许操作矩阵的唯一实现(非会员不可升级 / 不支持月转年与年转月 / 同档与降档拒绝 / 目标不可购买拒绝)。
|
||||
- `membership_cycle_remaining_ratio_ppm(cycle_started_at, cycle_resets_at, now) -> u32`:本期剩余比例(ppm,向下取整)。
|
||||
- **「后续完整月数」= `cycle_count − cycle_index`**,不从 `cycle_resets_at` 重新锚定推算:
|
||||
月末夹取后的日期(1/31 锚点得到 2/28)不再是原始开通日,重锚会把后续月份漂移成 3/28。
|
||||
- `apply_membership_upgrade(row, quote) -> row'`:`cycle_remaining += 补点`、
|
||||
`cycle_granted_points += 补点`、`plan` 更新,刷新日与到期日不变。
|
||||
- `is_unlimited_concurrency(limit) -> bool`:`limit >= 128` 视为不设上限(哨兵值集中在领域层一处)。
|
||||
|
||||
@@ -10,6 +10,8 @@ mod membership_limits;
|
||||
pub use membership_limits::*;
|
||||
mod membership_catalog;
|
||||
pub use membership_catalog::*;
|
||||
mod membership_upgrade;
|
||||
pub use membership_upgrade::*;
|
||||
mod application;
|
||||
pub use agc_models::*;
|
||||
mod commands;
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
//! 会员补差升级报价(纯函数,无 IO)。
|
||||
//!
|
||||
//! 规则来自实施计划 §3.3,并且**金额与取整只有这一处实现**:报价接口与下单重算必须调用同一个函数,
|
||||
//! 逐字段一致,前端展示价不是真相源。
|
||||
//!
|
||||
//! - 月付:`应付 = 新月价 − 当前月价`,`补点 = 新每期额度 − 当前每期额度`(不按时间折算)。
|
||||
//! - 年付:`应付 = (新年价 − 当前年价) / 12 × (后续完整月数 + 本期剩余比例)`,
|
||||
//! `补点 = (新每期额度 − 当前每期额度) × 本期剩余比例`。
|
||||
//! - 取整:金额**向上**取到分,补点**向下**取整。
|
||||
//! - 有效期、刷新日、期数都不变;已用点不返还,永久泥点不动。
|
||||
//!
|
||||
//! 「后续完整月数」取 `cycle_count − cycle_index`,**不**从 `cycle_resets_at` 重新锚定推算:
|
||||
//! 月末夹取后的日期(如 1/31 锚点得到 2/28)不再是原始开通日,重锚会漂移成 3/28。
|
||||
//! 「本期剩余比例」用本期实际时长做分母,单位取百万分之一(ppm)。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::membership_catalog::resolve_runtime_profile_membership_plan_snapshot;
|
||||
use crate::membership_cycle::membership_cycle_remaining_micros;
|
||||
use crate::{RuntimeProfileMembershipCycleKind, RuntimeProfileMembershipPlan};
|
||||
|
||||
/// 比例分母:百万分之一。
|
||||
const REMAINING_RATIO_PPM: u128 = 1_000_000;
|
||||
/// 年付按 12 个自然月折算。
|
||||
const MONTHS_PER_YEAR: u128 = 12;
|
||||
|
||||
/// 报价接口与下单重算的共用输入。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
pub current_plan: RuntimeProfileMembershipPlan,
|
||||
pub current_cycle_kind: RuntimeProfileMembershipCycleKind,
|
||||
pub target_plan: RuntimeProfileMembershipPlan,
|
||||
pub target_cycle_kind: RuntimeProfileMembershipCycleKind,
|
||||
/// 当前期序号(1-based)。
|
||||
pub cycle_index: u32,
|
||||
/// 本次购买总期数(月付 1 / 年付 12)。
|
||||
pub cycle_count: u32,
|
||||
pub cycle_started_at_micros: i64,
|
||||
pub cycle_resets_at_micros: i64,
|
||||
pub expires_at_micros: i64,
|
||||
pub cycle_remaining_points: u64,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
|
||||
/// 补差升级报价结果;有效期与刷新日与变更前一致。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileMembershipUpgradeQuote {
|
||||
pub amount_cents: u64,
|
||||
pub granted_points_delta: u64,
|
||||
pub monthly_balance_after: u64,
|
||||
pub expires_at_micros: i64,
|
||||
pub cycle_resets_at_micros: i64,
|
||||
pub cycle_index: u32,
|
||||
pub remaining_full_months: u32,
|
||||
pub remaining_ratio_ppm: u32,
|
||||
}
|
||||
|
||||
/// 升级被拒绝的原因;对外应映射为 4xx 与可读文案,而不是静默算出金额。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RuntimeProfileMembershipUpgradeRejection {
|
||||
/// 当前不是会员:应走下单购买流程,而不是补差升级。
|
||||
NotAMember,
|
||||
/// 月转年 / 年转月:本期不支持即时转换,到期后按完整价格重新选择。
|
||||
CycleKindChangeUnsupported,
|
||||
/// 同档位(重复购买)或降档:不允许,到期后再重新选择。
|
||||
NotUpgrade,
|
||||
/// 目标档位不可购买(非会员占位或已下架)。
|
||||
TargetPlanNotPurchasable,
|
||||
/// 目录缺少对应档位 / 周期的价格。
|
||||
MissingCatalogEntry,
|
||||
}
|
||||
|
||||
impl RuntimeProfileMembershipUpgradeRejection {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NotAMember => "not_a_member",
|
||||
Self::CycleKindChangeUnsupported => "cycle_kind_change_unsupported",
|
||||
Self::NotUpgrade => "not_upgrade",
|
||||
Self::TargetPlanNotPurchasable => "target_plan_not_purchasable",
|
||||
Self::MissingCatalogEntry => "missing_catalog_entry",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 本期剩余比例(ppm):`剩余微秒 / 本期实际微秒`,向下取整,已过期或时长非法时为 0。
|
||||
pub fn membership_cycle_remaining_ratio_ppm(
|
||||
cycle_started_at_micros: i64,
|
||||
cycle_resets_at_micros: i64,
|
||||
now_micros: i64,
|
||||
) -> u32 {
|
||||
let total_micros = cycle_resets_at_micros.saturating_sub(cycle_started_at_micros);
|
||||
if total_micros <= 0 {
|
||||
return 0;
|
||||
}
|
||||
let remaining_micros = membership_cycle_remaining_micros(cycle_resets_at_micros, now_micros);
|
||||
let remaining_micros = remaining_micros.min(total_micros);
|
||||
(u128::from(remaining_micros as u64) * REMAINING_RATIO_PPM / u128::from(total_micros as u64))
|
||||
as u32
|
||||
}
|
||||
|
||||
/// 允许操作矩阵(实施计划 §3.4):只有「已是会员 + 同周期 + 升更高档位 + 目标可购买」才允许升级。
|
||||
pub fn check_runtime_profile_membership_upgrade_allowed(
|
||||
current_plan: RuntimeProfileMembershipPlan,
|
||||
target_plan: RuntimeProfileMembershipPlan,
|
||||
current_cycle_kind: RuntimeProfileMembershipCycleKind,
|
||||
target_cycle_kind: RuntimeProfileMembershipCycleKind,
|
||||
) -> Result<(), RuntimeProfileMembershipUpgradeRejection> {
|
||||
if !current_plan.is_purchasable() {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::NotAMember);
|
||||
}
|
||||
if current_cycle_kind != target_cycle_kind {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::CycleKindChangeUnsupported);
|
||||
}
|
||||
if crate::runtime_profile_membership_plan_rank(target_plan)
|
||||
<= crate::runtime_profile_membership_plan_rank(current_plan)
|
||||
{
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::NotUpgrade);
|
||||
}
|
||||
if !crate::is_runtime_profile_membership_plan_purchasable(target_plan) {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::TargetPlanNotPurchasable);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 计算补差升级报价;报价与下单重算必须共用本函数。
|
||||
pub fn quote_runtime_profile_membership_upgrade(
|
||||
input: RuntimeProfileMembershipUpgradeQuoteInput,
|
||||
) -> Result<RuntimeProfileMembershipUpgradeQuote, RuntimeProfileMembershipUpgradeRejection> {
|
||||
check_runtime_profile_membership_upgrade_allowed(
|
||||
input.current_plan,
|
||||
input.target_plan,
|
||||
input.current_cycle_kind,
|
||||
input.target_cycle_kind,
|
||||
)?;
|
||||
|
||||
let current = resolve_runtime_profile_membership_plan_snapshot(
|
||||
input.current_plan,
|
||||
input.current_cycle_kind,
|
||||
)
|
||||
.ok_or(RuntimeProfileMembershipUpgradeRejection::MissingCatalogEntry)?;
|
||||
let target = resolve_runtime_profile_membership_plan_snapshot(
|
||||
input.target_plan,
|
||||
input.target_cycle_kind,
|
||||
)
|
||||
.ok_or(RuntimeProfileMembershipUpgradeRejection::MissingCatalogEntry)?;
|
||||
|
||||
let remaining_full_months = input.cycle_count.saturating_sub(input.cycle_index);
|
||||
let remaining_ratio_ppm = membership_cycle_remaining_ratio_ppm(
|
||||
input.cycle_started_at_micros,
|
||||
input.cycle_resets_at_micros,
|
||||
input.now_micros,
|
||||
);
|
||||
|
||||
let price_delta_cents = target.price_cents.saturating_sub(current.price_cents);
|
||||
let points_delta = target.period_points.saturating_sub(current.period_points);
|
||||
|
||||
let (amount_cents, granted_points_delta) = match input.target_cycle_kind {
|
||||
RuntimeProfileMembershipCycleKind::Monthly => (price_delta_cents, points_delta),
|
||||
RuntimeProfileMembershipCycleKind::Yearly => {
|
||||
let weighted_months_ppm = u128::from(remaining_full_months) * REMAINING_RATIO_PPM
|
||||
+ u128::from(remaining_ratio_ppm);
|
||||
let amount_cents = ceil_div(
|
||||
u128::from(price_delta_cents) * weighted_months_ppm,
|
||||
MONTHS_PER_YEAR * REMAINING_RATIO_PPM,
|
||||
);
|
||||
let granted_points_delta = (u128::from(points_delta) * u128::from(remaining_ratio_ppm)
|
||||
/ REMAINING_RATIO_PPM) as u64;
|
||||
(amount_cents, granted_points_delta)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RuntimeProfileMembershipUpgradeQuote {
|
||||
amount_cents,
|
||||
granted_points_delta,
|
||||
monthly_balance_after: input
|
||||
.cycle_remaining_points
|
||||
.saturating_add(granted_points_delta),
|
||||
expires_at_micros: input.expires_at_micros,
|
||||
cycle_resets_at_micros: input.cycle_resets_at_micros,
|
||||
cycle_index: input.cycle_index,
|
||||
remaining_full_months,
|
||||
remaining_ratio_ppm,
|
||||
})
|
||||
}
|
||||
|
||||
/// 向上取整的整数除法(金额取整到分)。
|
||||
fn ceil_div(numerator: u128, denominator: u128) -> u64 {
|
||||
if denominator == 0 {
|
||||
return 0;
|
||||
}
|
||||
((numerator + denominator - 1) / denominator) as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::membership_cycle::beijing_local_micros;
|
||||
|
||||
const DAY_MICROS: i64 = 86_400_000_000;
|
||||
|
||||
fn beijing(year: i32, month: u8, day: u8, hour: i64) -> i64 {
|
||||
beijing_local_micros(year, month, day, hour * 3_600_000_000)
|
||||
}
|
||||
|
||||
fn base_input() -> RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
let cycle_started_at_micros = beijing(2026, 6, 20, 9);
|
||||
let cycle_resets_at_micros = beijing(2026, 7, 20, 9);
|
||||
RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_plan: RuntimeProfileMembershipPlan::Plus,
|
||||
current_cycle_kind: RuntimeProfileMembershipCycleKind::Yearly,
|
||||
target_plan: RuntimeProfileMembershipPlan::Pro,
|
||||
target_cycle_kind: RuntimeProfileMembershipCycleKind::Yearly,
|
||||
cycle_index: 3,
|
||||
cycle_count: 9,
|
||||
cycle_started_at_micros,
|
||||
cycle_resets_at_micros,
|
||||
expires_at_micros: beijing(2027, 3, 20, 9),
|
||||
cycle_remaining_points: 615,
|
||||
now_micros: cycle_started_at_micros
|
||||
+ (cycle_resets_at_micros - cycle_started_at_micros) / 2,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monthly_upgrade_matches_the_spec_example() {
|
||||
// 中文注释:Plus 月 → Pro 月:补付 ¥200、补点 2500、余额 615 + 2500 = 3115。
|
||||
let quote =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_cycle_kind: RuntimeProfileMembershipCycleKind::Monthly,
|
||||
target_cycle_kind: RuntimeProfileMembershipCycleKind::Monthly,
|
||||
cycle_index: 1,
|
||||
cycle_count: 1,
|
||||
cycle_remaining_points: 615,
|
||||
..base_input()
|
||||
})
|
||||
.expect("月付升级应可报价");
|
||||
|
||||
assert_eq!(quote.amount_cents, 20_000);
|
||||
assert_eq!(quote.granted_points_delta, 2_500);
|
||||
assert_eq!(quote.monthly_balance_after, 3_115);
|
||||
assert_eq!(quote.remaining_full_months, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yearly_upgrade_matches_the_spec_example() {
|
||||
// 中文注释:Plus 年 → Pro 年,剩 6 个完整月 + 半个当前月 → 补付 ¥1083.34、补点 1250。
|
||||
let quote =
|
||||
quote_runtime_profile_membership_upgrade(base_input()).expect("年付升级应可报价");
|
||||
|
||||
assert_eq!(quote.amount_cents, 108_334);
|
||||
assert_eq!(quote.granted_points_delta, 1_250);
|
||||
assert_eq!(quote.monthly_balance_after, 1_865);
|
||||
assert_eq!(quote.remaining_full_months, 6);
|
||||
assert_eq!(quote.remaining_ratio_ppm, 500_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_keeps_expiry_reset_and_cycle_index() {
|
||||
let input = base_input();
|
||||
let quote = quote_runtime_profile_membership_upgrade(input).expect("年付升级应可报价");
|
||||
assert_eq!(quote.expires_at_micros, input.expires_at_micros);
|
||||
assert_eq!(quote.cycle_resets_at_micros, input.cycle_resets_at_micros);
|
||||
assert_eq!(quote.cycle_index, input.cycle_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_ratio_is_zero_at_or_after_reset() {
|
||||
let input = base_input();
|
||||
assert_eq!(
|
||||
membership_cycle_remaining_ratio_ppm(
|
||||
input.cycle_started_at_micros,
|
||||
input.cycle_resets_at_micros,
|
||||
input.cycle_resets_at_micros
|
||||
),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
membership_cycle_remaining_ratio_ppm(
|
||||
input.cycle_started_at_micros,
|
||||
input.cycle_resets_at_micros,
|
||||
input.cycle_resets_at_micros + DAY_MICROS
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
let quote =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
now_micros: input.cycle_resets_at_micros + DAY_MICROS,
|
||||
..input
|
||||
})
|
||||
.expect("年付升级应可报价");
|
||||
assert_eq!(quote.remaining_ratio_ppm, 0);
|
||||
assert_eq!(quote.granted_points_delta, 0);
|
||||
// 中文注释:仅剩 6 个完整月,本期零头为 0,因此只收 6/12 的价差并向下取整补点。
|
||||
assert_eq!(quote.amount_cents, 100_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_member_must_purchase_instead_of_upgrading() {
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_plan: RuntimeProfileMembershipPlan::Normal,
|
||||
target_plan: RuntimeProfileMembershipPlan::Starter,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::NotAMember)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_plan_and_downgrade_are_rejected() {
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
target_plan: RuntimeProfileMembershipPlan::Plus,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::NotUpgrade)
|
||||
);
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_plan: RuntimeProfileMembershipPlan::Pro,
|
||||
target_plan: RuntimeProfileMembershipPlan::Plus,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::NotUpgrade)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_kind_change_is_rejected() {
|
||||
assert_eq!(
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_cycle_kind: RuntimeProfileMembershipCycleKind::Monthly,
|
||||
target_cycle_kind: RuntimeProfileMembershipCycleKind::Yearly,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::CycleKindChangeUnsupported)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuing_upgrade_compares_against_the_current_plan() {
|
||||
// 中文注释:Starter → Plus → Pro 时,第二次按 Plus vs Pro 计算(¥1083.34),
|
||||
// 而不是按 Starter 直接跳 Pro(¥1408.34)。
|
||||
let starter_to_plus =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_plan: RuntimeProfileMembershipPlan::Starter,
|
||||
target_plan: RuntimeProfileMembershipPlan::Plus,
|
||||
cycle_remaining_points: 0,
|
||||
..base_input()
|
||||
})
|
||||
.expect("Starter → Plus 应可报价");
|
||||
let starter_to_pro =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
current_plan: RuntimeProfileMembershipPlan::Starter,
|
||||
target_plan: RuntimeProfileMembershipPlan::Pro,
|
||||
cycle_remaining_points: 0,
|
||||
..base_input()
|
||||
})
|
||||
.expect("Starter → Pro 应可报价");
|
||||
let plus_to_pro =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
cycle_remaining_points: 0,
|
||||
..base_input()
|
||||
})
|
||||
.expect("Plus → Pro 应可报价");
|
||||
|
||||
assert_eq!(starter_to_plus.amount_cents, 32_500);
|
||||
assert_eq!(starter_to_pro.amount_cents, 140_834);
|
||||
assert_eq!(plus_to_pro.amount_cents, 108_334);
|
||||
assert_ne!(starter_to_pro.amount_cents, plus_to_pro.amount_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yearly_amount_rounds_up_to_cent_and_points_round_down() {
|
||||
// 中文注释:本期实际时长 3 天、已过 2 天 → 剩余比例 333_333 ppm(向下取整);
|
||||
// 金额 200000 × (6 + 0.333333) / 12 = 105555.55 分 → 向上到 105556;
|
||||
// 补点 2500 × 0.333333 = 833.33 → 向下取整为 833。
|
||||
let started = beijing(2026, 6, 20, 9);
|
||||
let quote =
|
||||
quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput {
|
||||
cycle_started_at_micros: started,
|
||||
cycle_resets_at_micros: started + 3 * DAY_MICROS,
|
||||
now_micros: started + 2 * DAY_MICROS,
|
||||
..base_input()
|
||||
})
|
||||
.expect("年付升级应可报价");
|
||||
|
||||
assert_eq!(quote.remaining_ratio_ppm, 333_333);
|
||||
assert_eq!(quote.amount_cents, 105_556);
|
||||
assert_eq!(quote.granted_points_delta, 833);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user