feat: add expiring reset card wallet

This commit is contained in:
2026-09-08 23:04:54 +08:00
parent 91ae7912ab
commit 01525834fd
8 changed files with 943 additions and 5 deletions

View File

@@ -694,6 +694,182 @@ describe('works square host api routes', () => {
});
});
it('lists reset cards through a strict Renderer-safe projection', async () => {
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(JSON.stringify({
items: [
{
id: 'card-available',
status: 'available',
granted_at: '2026-09-08T00:00:00Z',
expires_at: '2099-09-15T00:00:00Z',
redeemed_at: null,
redeemed_cycle_id: null,
reason: 'internal operations note',
granted_by_user_id: 'private-admin-id',
},
{
id: 'card-redeemed',
status: 'redeemed',
granted_at: '2026-09-01T00:00:00Z',
expires_at: '2099-09-10T00:00:00Z',
redeemed_at: '2026-09-05T00:00:00Z',
redeemed_cycle_id: 'cycle-2',
},
{
id: 'card-stale',
status: 'available',
granted_at: '1999-12-01T00:00:00Z',
expires_at: '2000-01-01T00:00:00Z',
redeemed_at: null,
redeemed_cycle_id: null,
},
],
}), { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
const response = createResponse();
const handled = await handleWorksRoutes(
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/works/billing/reset-cards'),
{} as never,
);
expect(handled).toBe(true);
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
success: true,
cards: [
{
id: 'card-available',
status: 'available',
granted_at: '2026-09-08T00:00:00Z',
expires_at: '2099-09-15T00:00:00Z',
redeemed_at: null,
redeemed_cycle_id: null,
},
{
id: 'card-redeemed',
status: 'redeemed',
granted_at: '2026-09-01T00:00:00Z',
expires_at: '2099-09-10T00:00:00Z',
redeemed_at: '2026-09-05T00:00:00Z',
redeemed_cycle_id: 'cycle-2',
},
{
id: 'card-stale',
status: 'expired',
granted_at: '1999-12-01T00:00:00Z',
expires_at: '2000-01-01T00:00:00Z',
redeemed_at: null,
redeemed_cycle_id: null,
},
],
});
expect(JSON.stringify(response.json())).not.toContain('private-admin-id');
expect(fetchMock).toHaveBeenCalledWith(
'https://square.nianxx.cn/api/billing/reset-cards',
{
method: 'GET',
headers: { Authorization: 'Bearer access-token' },
},
);
});
it('rejects an invalid reset-card list instead of partially forwarding it', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(JSON.stringify({
items: [{
id: 'card-invalid',
status: 'available',
granted_at: 'not-a-time',
expires_at: '2099-09-15T00:00:00Z',
redeemed_at: null,
redeemed_cycle_id: null,
}],
}), { status: 200 })));
const response = createResponse();
await handleWorksRoutes(
createRequest('GET', undefined, { 'x-niancode-access-token': 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/works/billing/reset-cards'),
{} as never,
);
expect(response.statusCode).toBe(502);
expect(response.json()).toEqual({
success: false,
error: 'Works Square returned an invalid reset-card list',
});
});
it('redeems one reset card and projects only the fulfilled card contract', async () => {
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(JSON.stringify({
id: 'card/one',
status: 'redeemed',
granted_at: '2026-09-08T00:00:00Z',
expires_at: '2099-09-15T00:00:00Z',
redeemed_at: '2026-09-09T00:00:00Z',
redeemed_cycle_id: 'cycle-new',
metadata_json: { internal: true },
}), { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
const response = createResponse();
await handleWorksRoutes(
createRequest('POST', undefined, { 'x-niancode-access-token': 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/works/billing/reset-cards/card%2Fone/redeem'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
success: true,
card: {
id: 'card/one',
status: 'redeemed',
granted_at: '2026-09-08T00:00:00Z',
expires_at: '2099-09-15T00:00:00Z',
redeemed_at: '2026-09-09T00:00:00Z',
redeemed_cycle_id: 'cycle-new',
},
});
expect(fetchMock).toHaveBeenCalledWith(
'https://square.nianxx.cn/api/billing/reset-cards/card%2Fone/redeem',
{
method: 'POST',
headers: { Authorization: 'Bearer access-token' },
},
);
});
it('maps reset-card failures to a closed safe error without upstream text', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(JSON.stringify({
detail: {
error_code: 'reset_card_expired',
message: 'private upstream diagnostics must not reach Renderer',
},
}), { status: 409 })));
const response = createResponse();
await handleWorksRoutes(
createRequest('POST', undefined, { 'x-niancode-access-token': 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/works/billing/reset-cards/card-expired/redeem'),
{} as never,
);
expect(response.statusCode).toBe(409);
expect(response.json()).toEqual({
success: false,
status: 409,
code: 'reset_card_expired',
error: '这张重置卡已过期。',
});
expect(JSON.stringify(response.json())).not.toContain('private upstream');
});
it('removes exact plan and point values from a family-shared balance', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify({