Feat/陶泥儿会员订阅方案 #588
@@ -1521,6 +1521,7 @@ fn is_runtime_profile_membership_domain_error(message: &str) -> bool {
|
||||
"当前不是会员,请直接选择套餐购买"
|
||||
| "当前套餐已生效,无需重复购买"
|
||||
| "当前会员有效期内不支持月付 / 年付转换,到期后可重新选择"
|
||||
| "会员已到期,请按新购价格重新下单"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ pub use llm_billing::*;
|
||||
pub use membership::{
|
||||
MEMBERSHIP_UNLIMITED_CONCURRENCY, RuntimeProfileMembershipPlanRecord,
|
||||
RuntimeProfileMembershipPlanSnapshot, RuntimeProfileMembershipUpgradeQuoteInput,
|
||||
beijing_local_micros, build_runtime_profile_membership_plan_snapshot, is_unlimited_concurrency,
|
||||
membership_cycle_window, membership_expires_at, parse_runtime_profile_membership_product_id,
|
||||
beijing_local_micros, build_runtime_profile_membership_plan_snapshot,
|
||||
check_expired_membership_upgrade_settlement, is_unlimited_concurrency, membership_cycle_window,
|
||||
membership_expires_at, parse_runtime_profile_membership_product_id,
|
||||
quote_runtime_profile_membership_upgrade, resolve_runtime_profile_membership_plan_row,
|
||||
runtime_profile_membership_plan_catalog, runtime_profile_membership_plan_rank,
|
||||
runtime_profile_membership_product_id,
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
//!
|
||||
//! 入参要求 `cycle_started_at <= now < cycle_resets_at`:调用方必须先把会员行投射到当前账期,
|
||||
//! 不接受已刷新的旧窗口——否则剩余比例与期号口径不一致,会静默多收一个完整月。
|
||||
//!
|
||||
//! 报价冻结后会员可能在支付前到期,此时补差价不再覆盖整期新购价,
|
||||
//! 由 [`check_expired_membership_upgrade_settlement`] 在结算侧判定。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -74,6 +77,8 @@ pub enum RuntimeProfileMembershipUpgradeRejection {
|
||||
TargetPlanNotPurchasable,
|
||||
/// 报价入参的数值不变量被破坏(期号越界、账期窗口非法或 `now` 不在窗口内):拒绝,而不是静默夹取。
|
||||
InvalidQuoteInput,
|
||||
/// 补差价订单结算时会员已到期,且订单金额不够覆盖整期新购价:应重新下单购买。
|
||||
MembershipExpiredBeforeSettlement,
|
||||
}
|
||||
|
||||
impl RuntimeProfileMembershipUpgradeRejection {
|
||||
@@ -84,6 +89,7 @@ impl RuntimeProfileMembershipUpgradeRejection {
|
||||
Self::NotUpgrade => "not_upgrade",
|
||||
Self::TargetPlanNotPurchasable => "target_plan_not_purchasable",
|
||||
Self::InvalidQuoteInput => "invalid_quote_input",
|
||||
Self::MembershipExpiredBeforeSettlement => "membership_expired_before_settlement",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +216,21 @@ pub fn quote_runtime_profile_membership_upgrade(
|
||||
})
|
||||
}
|
||||
|
||||
/// 补差价订单在「会员已到期」时是否仍可按新购结算。
|
||||
///
|
||||
/// 补差价只覆盖升级当期剩余权益的差额,前提是升级时会员仍在有效期内;一旦拖到到期后才支付,
|
||||
/// 同一笔冻结金额就不再覆盖整期新购价。只有订单金额达到目标周期当前目录价才继续按新购结算,
|
||||
/// 否则拒绝,避免用零头价换一整个周期。
|
||||
pub fn check_expired_membership_upgrade_settlement(
|
||||
order_amount_cents: u64,
|
||||
target_price_cents: u64,
|
||||
) -> Result<(), RuntimeProfileMembershipUpgradeRejection> {
|
||||
if order_amount_cents < target_price_cents {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::MembershipExpiredBeforeSettlement);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 向上取整的整数除法(金额取整到分)。
|
||||
///
|
||||
/// 分母是 `MONTHS_PER_YEAR * REMAINING_RATIO_PPM` 这类编译期常量,恒不为 0;
|
||||
@@ -551,4 +572,31 @@ mod tests {
|
||||
assert_eq!(quote.amount_cents, 155_556);
|
||||
assert_eq!(quote.granted_points_delta, 833);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_settlement_needs_the_full_target_price() {
|
||||
// 中文注释:年付 Pro 目录价 299_000 分;补差价 158_334 分覆盖不了整期新购价,必须拒绝,
|
||||
// 否则用户可在到期前用补差价建单、到期后再支付,等于用零头价换一个完整周期。
|
||||
let pro_yearly = snapshot(
|
||||
RuntimeProfileMembershipPlan::Pro,
|
||||
RuntimeProfileMembershipCycleKind::Yearly,
|
||||
);
|
||||
assert_eq!(pro_yearly.price_cents, 299_000);
|
||||
assert_eq!(
|
||||
check_expired_membership_upgrade_settlement(158_334, pro_yearly.price_cents),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::MembershipExpiredBeforeSettlement)
|
||||
);
|
||||
// 中文注释:订单金额本身已覆盖整期新购价(改价后挡位降价的容错路径)时按新购结算。
|
||||
assert_eq!(
|
||||
check_expired_membership_upgrade_settlement(pro_yearly.price_cents, 299_000),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
check_expired_membership_upgrade_settlement(
|
||||
pro_yearly.price_cents + 1,
|
||||
pro_yearly.price_cents
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9832,7 +9832,9 @@ fn build_profile_membership_snapshot(
|
||||
/// - 结算口径以订单建单时冻结的快照为准:档位、每期泥点、补点与价格都不再回查目录,
|
||||
/// 因此下单后改价 / 改每期泥点 / 下架档位都不会影响已受理订单;历史 Pending 订单回退到按目录重算。
|
||||
/// - 有效期内:只允许「同周期升更高档位」,按快照补点,刷新日与到期日不变。
|
||||
/// - 无有效会员(从未开通或已到期):按快照重新起算,锚点就是本次支付时刻。
|
||||
/// - 无有效会员(从未开通或已到期):按快照重新起算,锚点就是本次支付时刻;
|
||||
/// 但补差价订单结算时会员已到期,必须先满足 [`check_expired_membership_upgrade_settlement`],
|
||||
/// 否则拒绝——补差价只覆盖当期剩余权益的差额,不覆盖整期新购价。
|
||||
///
|
||||
/// 幂等锚点是**订单维度**的账本 id,重复支付回调天然幂等。
|
||||
fn apply_profile_membership_purchase(
|
||||
@@ -9872,7 +9874,13 @@ fn apply_profile_membership_purchase(
|
||||
purchased_at,
|
||||
);
|
||||
}
|
||||
// 中文注释:会员在支付前已到期,退化为按冻结档位重新起算,避免已付款订单卡死。
|
||||
// 中文注释:会员在支付前已到期,冻结的补差价只覆盖当期剩余权益的差额,
|
||||
// 不再覆盖整期新购价;只有订单金额达到目标周期当前目录价才按新购结算,
|
||||
// 否则拒绝,避免用户用零头补差价换到一个完整周期。
|
||||
let target = membership_plan_snapshot(ctx, settlement.plan, settlement.cycle_kind)
|
||||
.ok_or_else(|| format!("会员目录缺少档位 {}", settlement.plan.as_str()))?;
|
||||
check_expired_membership_upgrade_settlement(order.amount_cents, target.price_cents)
|
||||
.map_err(|_| "会员已到期,请按新购价格重新下单".to_string())?;
|
||||
}
|
||||
|
||||
apply_profile_membership_fresh_purchase(
|
||||
|
||||
Reference in New Issue
Block a user