fix(充值弹窗): 当前档位改从未过滤目录解析

- 有效会员的当前档位被后台临时禁用时,filter 后的 membershipPlans 查不到,状态卡退化成「会员」、按钮变「开通会员」,可在有效会员上重复开通(评审第 5 条)
- 结算弹窗升级摘要的 currentPlan 同样取自过滤列表,禁用档位会丢「原套餐标准价」;改由父组件传入未过滤目录解析出的 currentPlan,并移除弹窗对 membership 的无用依赖(评审第 11 条)
- 新增回归:active 会员 + 当前档 disabled 时仍显示「当前 Plus · 年付」、Max 提供「补差升级」,摘要保留「原套餐标准年价」
This commit is contained in:
2026-10-04 17:12:55 +08:00
parent 9476290c4b
commit 55bbf27e8f
2 changed files with 54 additions and 7 deletions
@@ -261,6 +261,50 @@ describe('PlatformProfileRechargeModal', () => {
expect(onSubmitMembershipCheckout).toHaveBeenCalledWith('max', 'yearly');
});
test('keeps an active member as member when their current plan is disabled', async () => {
const user = userEvent.setup();
const onRequestMembershipQuote = vi.fn();
render(
<PlatformProfileRechargeModal
center={{
mudPointBalance: buildMudPointBalance(615),
membership: buildActiveMembership('plus', 'yearly'),
pointProducts: buildPointProducts(),
// plus 在后台被临时禁用:目录里仍有该档,但过滤后的可购买列表只剩 max。
membershipPlans: [{ ...PLUS_PLAN, enabled: false }, MAX_PLAN],
latestOrder: null,
hasPointsRecharged: true,
}}
isLoading={false}
error={null}
submittingProductId={null}
nativePayment={null}
membershipQuote={null}
isLoadingMembershipQuote={false}
membershipQuoteError={null}
onClose={vi.fn()}
onRetry={vi.fn()}
onBuy={vi.fn()}
onRequestMembershipQuote={onRequestMembershipQuote}
onSubmitMembershipCheckout={vi.fn()}
onConfirmNativePayment={vi.fn()}
onCloseNativePayment={vi.fn()}
/>,
);
// 状态卡仍认得当前档位,而不是退化成「会员」。
expect(screen.getByText('当前 Plus · 年付')).toBeTruthy();
// 有效会员看到的是补差升级,而不是可在有效会员上重复开通的「开通会员」。
await user.click(screen.getByRole('button', { name: 'Max 年付 补差升级' }));
expect(onRequestMembershipQuote).toHaveBeenCalledWith('max');
const checkout = screen.getByRole('dialog', { name: '确认套餐变更' });
// 即使原档位已禁用,升级摘要也要给出「原套餐标准年价」。
expect(within(checkout).getByText('原套餐标准年价')).toBeTruthy();
expect(within(checkout).getByText('¥990')).toBeTruthy();
});
test('shows a placeholder instead of the list price while the upgrade quote is loading', async () => {
const user = userEvent.setup();
@@ -936,7 +936,7 @@ function PlatformPointProductGrid({
function PlatformRechargeCheckoutDialog({
target,
membershipPlans,
membership,
currentPlan,
membershipQuote,
isLoadingMembershipQuote,
membershipQuoteError,
@@ -949,7 +949,8 @@ function PlatformRechargeCheckoutDialog({
}: {
target: PlatformProfileRechargeCheckoutTarget;
membershipPlans: ProfileMembershipPlanRecord[];
membership: ProfileMembership | null;
/** 会员当前档位;来自未过滤目录,档位被后台临时禁用时仍保留「原套餐标准价」。 */
currentPlan: ProfileMembershipPlanRecord | null;
membershipQuote: ProfileMembershipUpgradeQuote | null;
isLoadingMembershipQuote: boolean;
membershipQuoteError: string | null;
@@ -1001,8 +1002,6 @@ function PlatformRechargeCheckoutDialog({
? (plan?.yearPriceCents ?? 0)
: (plan?.monthPriceCents ?? 0);
const isUpgrade = target.mode === 'upgrade';
const currentPlan =
membershipPlans.find((item) => item.plan === membership?.plan) ?? null;
const productId = resolveMembershipProductId(target.plan, target.cycleKind);
const submitting = submittingProductId === productId;
const quoteForTarget =
@@ -1317,10 +1316,14 @@ export function PlatformProfileRechargeModal({
() => center?.pointProducts ?? [],
[center?.pointProducts],
);
// 当前档位必须从「未过滤目录」里查:会员有效期内若该档被后台临时禁用,
// 过滤后的 membershipPlans 查不到会把有效会员误判成非会员(状态卡丢标题、按钮变「开通会员」)。
const currentPlanRecord = useMemo(
() =>
membershipPlans.find((plan) => plan.plan === membership?.plan) ?? null,
[membership?.plan, membershipPlans],
(center?.membershipPlans ?? []).find(
(plan) => plan.plan === membership?.plan,
) ?? null,
[center?.membershipPlans, membership?.plan],
);
const [activeTab, setActiveTab] =
useState<PlatformProfileRechargeTab>('subscription');
@@ -1546,7 +1549,7 @@ export function PlatformProfileRechargeModal({
<PlatformRechargeCheckoutDialog
target={checkout}
membershipPlans={membershipPlans}
membership={membership}
currentPlan={currentPlanRecord}
membershipQuote={membershipQuote}
isLoadingMembershipQuote={isLoadingMembershipQuote}
membershipQuoteError={membershipQuoteError}