feat(billing): guide parents to recharge the child account
This commit is contained in:
1 parent
fe1a3775c5
commit
281e6dc151
10 files changed
+105
-14
No files matched your search
@@ -4,7 +4,7 @@ type WalletFixture = { requests: string[]; paid: boolean; created: boolean };
|
||||
type FixtureGlobal = typeof globalThis & { __pointWalletE2E: WalletFixture };
|
||||
|
||||
test.describe('Permanent point wallet', () => {
|
||||
test('reopens an unpaid order, keeps its frozen amount, and refreshes only after confirmed credit', async ({ launchElectronApp }, testInfo) => {
|
||||
test('shows the child recipient, reopens its frozen unpaid order, and refreshes only after confirmed credit', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
@@ -40,12 +40,12 @@ test.describe('Permanent point wallet', () => {
|
||||
});
|
||||
if (path === '/api/auth/me') return result({
|
||||
success: true,
|
||||
user: { username: 'point-wallet', userId: 'point-wallet-user', tenantId: null, deptId: null, authorities: [] },
|
||||
user: { username: 'child-wallet', userId: 'child-wallet-user', tenantId: null, deptId: null, authorities: [] },
|
||||
moduleAccess: { programming: true, design: true, robot: true },
|
||||
});
|
||||
if (path === '/api/works/user/agent-profile') return result({
|
||||
success: true, profile: {
|
||||
display_name: '词元点数用户', age: null, gender: null, avatar_url: null,
|
||||
display_name: '小麦', age: null, gender: null, avatar_url: null,
|
||||
share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true,
|
||||
completed: true, version: 1, updated_at: '2026-09-22T00:00:00Z',
|
||||
},
|
||||
@@ -78,13 +78,15 @@ test.describe('Permanent point wallet', () => {
|
||||
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-member-menu-trigger')).toContainText('词元点数用户');
|
||||
await expect(page.getByTestId('sidebar-member-menu-trigger')).toContainText('小麦');
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await expect(page.getByTestId('sidebar-total-token-points')).toHaveText('0 点');
|
||||
await expect(page.getByText('点数已用尽,充值后可继续使用。')).toBeVisible();
|
||||
await expect(page.getByText('点数已用尽,请由家长完成当前账号充值。')).toBeVisible();
|
||||
await page.getByTestId('sidebar-point-wallet-open').click();
|
||||
const wallet = page.getByTestId('point-wallet-dialog');
|
||||
await expect(wallet).toContainText('1 元充值 50 点');
|
||||
await expect(wallet.getByTestId('point-recharge-recipient')).toHaveText('充值到:小麦(child-wallet)');
|
||||
await expect(wallet).toContainText('请由家长登录孩子的账号完成付款');
|
||||
await expect(wallet).not.toContainText('重置卡');
|
||||
await wallet.getByRole('button', { name: /500 点\s*¥10.00/ }).click();
|
||||
await expect(wallet.getByAltText('充值支付二维码')).toBeVisible();
|
||||
|
||||
@@ -27,14 +27,31 @@ describe('permanent point wallet', () => {
|
||||
expect(dialog).toHaveTextContent('永久有效');
|
||||
expect(screen.queryByText(/重置卡|升级订阅|本周剩余/)).not.toBeInTheDocument();
|
||||
});
|
||||
it('shows youth own precision and shared availability without enabling recharge', async () => {
|
||||
it('honors unavailable recharge without asking the user to change account identity', async () => {
|
||||
api.summary.mockResolvedValue(billingSummary({ can_recharge: false, payment_configured: false, own_points: permanentBalance({ can_recharge: false }), points: permanentBalance({ total: null, used: null, reserved: null, total_remaining: null, family_shared: true, entitlement_source: 'family_owner', shared_available: false, can_recharge: false }) }));
|
||||
render(<PointWallet />); await screen.findByText('75 点');
|
||||
expect(screen.getByText(/AI 编程使用共享点数/)).toHaveTextContent('已用尽');
|
||||
fireEvent.click(screen.getByTestId('sidebar-point-wallet-open'));
|
||||
await screen.findByText(/仅家长身份可以充值/);
|
||||
await screen.findByText(/当前账号暂不能充值,请在社区账户页查看原因或联系支持/);
|
||||
expect(screen.queryByText(/仅家长身份可以充值/)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /500 点\s*¥10.00/ })).toBeDisabled();
|
||||
});
|
||||
it('recharges the displayed child account while leaving shared funding selected', async () => {
|
||||
api.summary.mockResolvedValue(billingSummary({ points: permanentBalance({ total: null, used: null, reserved: null, total_remaining: null, family_shared: true, entitlement_source: 'shared_group', shared_available: true, can_recharge: false }) }));
|
||||
render(<PointWallet accountName="小麦" accountUsername="child-account" />);
|
||||
await screen.findByText('75 点');
|
||||
fireEvent.click(screen.getByTestId('sidebar-point-wallet-open'));
|
||||
const dialog = screen.getByTestId('point-wallet-dialog');
|
||||
expect(screen.getByTestId('point-recharge-recipient')).toHaveTextContent('充值到:小麦(child-account)');
|
||||
expect(dialog).toHaveTextContent('请由家长登录孩子的账号完成付款');
|
||||
expect(dialog).toHaveTextContent('充值不会自动切换付款来源');
|
||||
const buy = within(dialog).getByRole('button', { name: /500 点\s*¥10.00/ });
|
||||
await waitFor(() => expect(buy).toBeEnabled());
|
||||
fireEvent.click(buy);
|
||||
await screen.findByTestId('point-recharge-checkout');
|
||||
expect(api.create).toHaveBeenCalledWith('recharge-10', expect.any(String));
|
||||
expect(api.external).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not mistake the family payer’s own shared wallet for an exhausted external wallet', async () => {
|
||||
api.summary.mockResolvedValue(billingSummary({ points: permanentBalance({ family_shared: true, entitlement_source: 'shared_group' }) }));
|
||||
await openWallet();
|
||||
|
||||
@@ -91,6 +91,7 @@ describe('Sidebar permanent point wallet', () => {
|
||||
expect(screen.queryByText(/重置卡|升级订阅|会员等级|本周剩余/)).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('sidebar-point-wallet-open'));
|
||||
expect(screen.getByTestId('point-wallet-dialog')).toHaveTextContent('1 元充值 50 点');
|
||||
expect(screen.getByTestId('point-recharge-recipient')).toHaveTextContent('测试用户(member@example.com)');
|
||||
});
|
||||
it('shows a retryable failure without inventing a zero balance', async () => {
|
||||
fetchBillingSummaryMock.mockRejectedValue(new Error('offline'));
|
||||
|
||||
@@ -101,6 +101,12 @@ describe('permanent points Main contract', () => {
|
||||
const result = await call('/me');
|
||||
expect(result.status).toBe(409); expect(result.body.data).toBeUndefined();
|
||||
});
|
||||
it('does not translate an unavailable current-account recharge into a parent-identity requirement', async () => {
|
||||
reply({ detail: '当前账号暂不能充值词元点数' }, 403);
|
||||
const result = await call('/recharge/orders', 'POST', { productId: 'p', requestId: 'r' });
|
||||
expect(result.body).toMatchObject({ commandOutcome: 'definitive_failure', error: '当前账号暂不能充值,请在社区账户页查看原因或联系支持。' });
|
||||
expect(result.body.error).not.toContain('联系家长');
|
||||
});
|
||||
it.each(['/plans', '/reset-cards', '/subscriptions', '/reset-card-orders'])('retires %s without contacting payment services', async (path) => {
|
||||
expect((await call(path)).status).toBe(410); expect(net).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import server from '../fixtures/permanent-points-server.json';
|
||||
// an isolated SQLite database and a fake payment provider. All identities are synthetic.
|
||||
// Covers the real API's serialized decimals, timestamps, optional fields and order envelopes.
|
||||
describe('Works Square permanent-points API compatibility', () => {
|
||||
it('projects old accounts at zero and genuinely registered youth at 100, without a client gift', () => {
|
||||
it('projects balance and recharge permission from the captured pre-D01 server response without a client gift', () => {
|
||||
expect(projectBillingSummary(server.before).own_points.total_remaining).toBe('0.00');
|
||||
const youth = projectBillingSummary(server.youth);
|
||||
expect(youth.own_points.total_remaining).toBe('100.00');
|
||||
|
||||
Reference in new issue
Block a user