重构(会员升级): 目录倒挂用独立 rejection 与配置错误码
- 新增 RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted - 报价遇到价格 / 每期泥点相等或倒挂时返回该变体,不再复用 InvalidQuoteInput - rejection.error_code() 把目录倒挂映射为 InvalidPlanConfig,人读文案保留原因 - profile.rs 三处统一用 membership_upgrade_rejection_error,并重导出该 rejection 类型 - api-server 补 400 + invalid_plan_config 映射断言
This commit is contained in:
@@ -2656,6 +2656,11 @@ mod tests {
|
||||
RuntimeProfileMembershipErrorCode::PlanCatalogMissing,
|
||||
"会员目录缺少档位 pro",
|
||||
),
|
||||
// 目录价格 / 每期泥点倒挂属后端配置错误,与 UpgradeRejected 分开暴露机器码。
|
||||
(
|
||||
RuntimeProfileMembershipErrorCode::InvalidPlanConfig,
|
||||
"会员升级被拒绝: catalog_pricing_inverted",
|
||||
),
|
||||
] {
|
||||
let error = map_runtime_profile_client_error(SpacetimeClientError::ProcedureRejected {
|
||||
code: code.into(),
|
||||
|
||||
@@ -1506,7 +1506,7 @@ pub enum RuntimeProfileMembershipErrorCode {
|
||||
ProductIdUnparseable,
|
||||
/// 会员商品已迁移到档位目录,旧商品不再可用。
|
||||
MembershipProductRetired,
|
||||
/// 后台会员档位改价 / 改权益配置校验失败。
|
||||
/// 后台会员档位改价 / 改权益配置校验失败,或读取目录时发现价格 / 每期泥点倒挂。
|
||||
InvalidPlanConfig,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,13 @@ pub use llm_billing::*;
|
||||
pub use membership::{
|
||||
MEMBERSHIP_UNLIMITED_CONCURRENCY, PROFILE_MEMBERSHIP_RECOMMENDED_PLAN,
|
||||
RuntimeProfileMembershipPlanRecord, RuntimeProfileMembershipPlanSnapshot,
|
||||
RuntimeProfileMembershipUpgradeQuoteInput, 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,
|
||||
RuntimeProfileMembershipUpgradeQuoteInput, RuntimeProfileMembershipUpgradeRejection,
|
||||
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,
|
||||
validate_runtime_profile_membership_plan_row_against_catalog,
|
||||
};
|
||||
|
||||
|
||||
@@ -77,6 +77,11 @@ pub enum RuntimeProfileMembershipUpgradeRejection {
|
||||
TargetPlanNotPurchasable,
|
||||
/// 报价入参的数值不变量被破坏(期号越界、账期窗口非法或 `now` 不在窗口内):拒绝,而不是静默夹取。
|
||||
InvalidQuoteInput,
|
||||
/// 目录价格 / 每期泥点未随 rank 严格递增(相等或倒挂)。
|
||||
///
|
||||
/// 与 [`Self::InvalidQuoteInput`] 分开:这是后端目录配置错误,不是调用方入参错误,
|
||||
/// 对外应映射为配置类错误码(`invalid_plan_config`),便于监控区分。
|
||||
CatalogPricingInverted,
|
||||
/// 补差价订单结算时会员已到期,且订单金额不够覆盖整期新购价:应重新下单购买。
|
||||
MembershipExpiredBeforeSettlement,
|
||||
}
|
||||
@@ -89,9 +94,21 @@ impl RuntimeProfileMembershipUpgradeRejection {
|
||||
Self::NotUpgrade => "not_upgrade",
|
||||
Self::TargetPlanNotPurchasable => "target_plan_not_purchasable",
|
||||
Self::InvalidQuoteInput => "invalid_quote_input",
|
||||
Self::CatalogPricingInverted => "catalog_pricing_inverted",
|
||||
Self::MembershipExpiredBeforeSettlement => "membership_expired_before_settlement",
|
||||
}
|
||||
}
|
||||
|
||||
/// 对外 procedure 错误码:目录价格 / 每期泥点倒挂属后端配置错误,与调用方入参错误分开,
|
||||
/// 便于按机器码定位;其余拒绝仍统一为 `UpgradeRejected`。
|
||||
pub fn error_code(&self) -> crate::RuntimeProfileMembershipErrorCode {
|
||||
match self {
|
||||
Self::CatalogPricingInverted => {
|
||||
crate::RuntimeProfileMembershipErrorCode::InvalidPlanConfig
|
||||
}
|
||||
_ => crate::RuntimeProfileMembershipErrorCode::UpgradeRejected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 本期剩余比例(ppm):`剩余微秒 / 本期实际微秒`,向下取整,已过期或时长非法时为 0。
|
||||
@@ -184,8 +201,9 @@ pub fn quote_runtime_profile_membership_upgrade(
|
||||
|
||||
// 目录价格 / 每期泥点必须严格随 rank 递增;相等或倒挂都直接拒绝,
|
||||
// 否则 checked_sub 会把「相等」算成 0 元升级 / 0 补点,违背目录不变量。
|
||||
// 这是后端目录配置错误,用独立变体与调用方入参错误区分。
|
||||
if target.price_cents <= current.price_cents || target.period_points <= current.period_points {
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput);
|
||||
return Err(RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted);
|
||||
}
|
||||
let price_delta_cents = target.price_cents - current.price_cents;
|
||||
let points_delta = target.period_points - current.period_points;
|
||||
@@ -247,6 +265,7 @@ fn ceil_div(numerator: u128, denominator: u128) -> u64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::RuntimeProfileMembershipErrorCode;
|
||||
use crate::RuntimeProfileMembershipPlan;
|
||||
use crate::membership::catalog::resolve_runtime_profile_membership_plan_snapshot;
|
||||
use crate::membership::cycle::beijing_local_micros;
|
||||
@@ -485,7 +504,7 @@ mod tests {
|
||||
target: target.clone(),
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted)
|
||||
);
|
||||
|
||||
target.price_cents = current.price_cents + 1;
|
||||
@@ -496,7 +515,7 @@ mod tests {
|
||||
target: target.clone(),
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted)
|
||||
);
|
||||
|
||||
// 中文注释:相等同样破坏「严格随 rank 递增」,必须拒绝;旧实现用 checked_sub
|
||||
@@ -509,7 +528,17 @@ mod tests {
|
||||
target,
|
||||
..base_input()
|
||||
}),
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)
|
||||
Err(RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted)
|
||||
);
|
||||
|
||||
// 中文注释:目录倒挂对外映射为配置错误码,与调用方入参错误(UpgradeRejected)区分。
|
||||
assert_eq!(
|
||||
RuntimeProfileMembershipUpgradeRejection::CatalogPricingInverted.error_code(),
|
||||
RuntimeProfileMembershipErrorCode::InvalidPlanConfig
|
||||
);
|
||||
assert_eq!(
|
||||
RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput.error_code(),
|
||||
RuntimeProfileMembershipErrorCode::UpgradeRejected
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8764,12 +8764,7 @@ fn build_profile_membership_upgrade_quote_record(
|
||||
&target,
|
||||
ctx.timestamp,
|
||||
))
|
||||
.map_err(|rejection| {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
RuntimeProfileMembershipErrorCode::UpgradeRejected,
|
||||
format!("会员升级被拒绝: {}", rejection.as_str()),
|
||||
)
|
||||
})?;
|
||||
.map_err(membership_upgrade_rejection_error)?;
|
||||
|
||||
Ok(RuntimeProfileMembershipUpgradeQuoteSnapshot {
|
||||
current_plan: current.plan,
|
||||
@@ -8786,6 +8781,19 @@ fn build_profile_membership_upgrade_quote_record(
|
||||
})
|
||||
}
|
||||
|
||||
/// 会员升级拒绝 → procedure 错误码。
|
||||
///
|
||||
/// 目录价格 / 每期泥点倒挂是后端配置错误,映射为 `InvalidPlanConfig`,与客户端入参错误
|
||||
/// (`UpgradeRejected`)区分开,便于按错误码定位;人读文案仍带具体 rejection 原因。
|
||||
fn membership_upgrade_rejection_error(
|
||||
rejection: RuntimeProfileMembershipUpgradeRejection,
|
||||
) -> RuntimeProfileMembershipDomainError {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
rejection.error_code(),
|
||||
format!("会员升级被拒绝: {}", rejection.as_str()),
|
||||
)
|
||||
}
|
||||
|
||||
/// 后台改价 / 改权益。`rank` 与 `plan` 不可改:`plan` 是主键(即身份),`rank` 是升级比较基准。
|
||||
fn upsert_profile_membership_plan_record(
|
||||
ctx: &ReducerContext,
|
||||
@@ -10107,12 +10115,7 @@ fn build_profile_membership_order_snapshot(
|
||||
let quote = quote_runtime_profile_membership_upgrade(membership_upgrade_quote_input(
|
||||
&row, ¤t, &target, created_at,
|
||||
))
|
||||
.map_err(|rejection| {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
RuntimeProfileMembershipErrorCode::UpgradeRejected,
|
||||
format!("会员升级被拒绝: {}", rejection.as_str()),
|
||||
)
|
||||
})?;
|
||||
.map_err(membership_upgrade_rejection_error)?;
|
||||
Ok(ProfileMembershipOrderSnapshot {
|
||||
change_kind: RuntimeProfileMembershipChangeKind::Upgrade,
|
||||
plan,
|
||||
@@ -10731,12 +10734,7 @@ fn build_recomputed_profile_membership_settlement(
|
||||
&target,
|
||||
purchased_at,
|
||||
))
|
||||
.map_err(|rejection| {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
RuntimeProfileMembershipErrorCode::UpgradeRejected,
|
||||
format!("会员升级被拒绝: {}", rejection.as_str()),
|
||||
)
|
||||
})?;
|
||||
.map_err(membership_upgrade_rejection_error)?;
|
||||
Ok(ProfileMembershipSettlement {
|
||||
change_kind: RuntimeProfileMembershipChangeKind::Upgrade,
|
||||
plan,
|
||||
|
||||
Reference in New Issue
Block a user