领域层:新增会员补差升级报价纯函数

- 新增 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:
2026-10-02 14:11:54 +08:00
parent 33a4c8dbc3
commit 2b77fa08c3
3 changed files with 403 additions and 2 deletions
@@ -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` 视为不设上限(哨兵值集中在领域层一处)。