feat: add expiring reset card wallet
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user