/** * E2E:钱包流水一致性 * * 链路: * 1. reset → 余额 baseline * 2. transfer-in N → 看 /wallet/logs 出现 type=transfer_in 且 amount=N * 3. transfer-out M → 出现 type=transfer_out 且 amount=M * 4. 流水 total 增量为 in - out * 5. type 过滤:只查 transfer_in 不出现 transfer_out * * 与 05 不同:05 测单次 transfer 成功;06 测多笔后的流水呈现 + 过滤。 */ import { test, expect, request as pwRequest } from '@playwright/test'; import { playerLoginViaBypass, resetE2EPlayer } from './_helper'; const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase(); const IN_1 = 12_000; const IN_2 = 8_000; const OUT_1 = 5_000; test.beforeEach(async () => { await resetE2EPlayer(); }); async function ctxOf(token: string) { return pwRequest.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${token}` }, }); } async function transferIn(ctx: any, amount: number, key: string) { const r = await ctx.post('/api/v1/wallet/transfer-in', { data: { amount, currency: CURRENCY, idempotent_key: key }, }); expect(r.ok(), `transfer-in status=${r.status()}`).toBeTruthy(); } async function transferOut(ctx: any, amount: number, key: string) { const r = await ctx.post('/api/v1/wallet/transfer-out', { data: { amount, currency: CURRENCY, idempotent_key: key }, }); expect(r.ok(), `transfer-out status=${r.status()}`).toBeTruthy(); } test('transfer-in/out 后 /wallet/logs 流水一致性', async () => { const session = await playerLoginViaBypass(); const ctx = await ctxOf(session.access_token); const keyIn1 = `e2e-log-in1-${Date.now()}`; const keyIn2 = `e2e-log-in2-${Date.now()}`; const keyOut1 = `e2e-log-out1-${Date.now()}`; await transferIn(ctx, IN_1, keyIn1); await transferIn(ctx, IN_2, keyIn2); await transferOut(ctx, OUT_1, keyOut1); const r = await ctx.get(`/api/v1/wallet/logs?size=100¤cy=${CURRENCY}`); expect(r.ok(), `logs status=${r.status()}`).toBeTruthy(); const body = (await r.json()) as any; expect(body.code).toBe(0); expect(body.data.funding_mode).toBe('wallet'); expect(body.data.ledger_source).toBeTruthy(); const items: any[] = body.data.items; expect(items.length).toBeGreaterThanOrEqual(3); const ourKeys = new Set([keyIn1, keyIn2, keyOut1]); const ourTxns = items.filter((it) => ourKeys.has(it.idempotent_key)); expect(ourTxns.length).toBe(3); const ins = ourTxns.filter((it) => it.type === 'transfer_in'); const outs = ourTxns.filter((it) => it.type === 'transfer_out'); expect(ins.length).toBe(2); expect(outs.length).toBe(1); expect(ins.reduce((s, x) => s + Math.abs(x.amount_abs ?? x.amount), 0)).toBe(IN_1 + IN_2); expect(Math.abs(outs[0].amount_abs ?? outs[0].amount)).toBe(OUT_1); await ctx.dispose(); }); test('type=transfer_in 过滤:结果中不应出现 transfer_out', async () => { const session = await playerLoginViaBypass(); const ctx = await ctxOf(session.access_token); await transferIn(ctx, IN_1, `e2e-logf-in1-${Date.now()}`); await transferOut(ctx, OUT_1, `e2e-logf-out1-${Date.now()}`); const r = await ctx.get(`/api/v1/wallet/logs?type=transfer_in&size=100¤cy=${CURRENCY}`); const body = (await r.json()) as any; const items: any[] = body.data.items; expect(items.length).toBeGreaterThanOrEqual(1); for (const it of items) { expect(it.type).toBe('transfer_in'); } // 不能有 transfer_out expect(items.some((it) => it.type === 'transfer_out')).toBe(false); await ctx.dispose(); }); test('pending_reconcile 字段:本地 stub 走秒成功,列表应为空', async () => { const session = await playerLoginViaBypass(); const ctx = await ctxOf(session.access_token); await transferIn(ctx, IN_1, `e2e-logp-in1-${Date.now()}`); const r = await ctx.get(`/api/v1/wallet/logs?size=20¤cy=${CURRENCY}`); const body = (await r.json()) as any; expect(Array.isArray(body.data.pending_reconcile)).toBeTruthy(); expect(body.data.pending_reconcile.length).toBe(0); await ctx.dispose(); }); test('分页:page=1 size=2 + page=2 size=2 不重叠且能拼回完整列表', async () => { const session = await playerLoginViaBypass(); const ctx = await ctxOf(session.access_token); await transferIn(ctx, 100, `e2e-page-in1-${Date.now()}`); await transferIn(ctx, 200, `e2e-page-in2-${Date.now()}`); await transferIn(ctx, 300, `e2e-page-in3-${Date.now()}`); const r1 = await ctx.get(`/api/v1/wallet/logs?page=1&size=2&type=transfer_in¤cy=${CURRENCY}`); const r2 = await ctx.get(`/api/v1/wallet/logs?page=2&size=2&type=transfer_in¤cy=${CURRENCY}`); const b1 = (await r1.json()) as any; const b2 = (await r2.json()) as any; expect(b1.data.items.length).toBeLessThanOrEqual(2); expect(b2.data.items.length).toBeLessThanOrEqual(2); const ids1 = new Set(b1.data.items.map((x: any) => x.log_id ?? x.id)); for (const it of b2.data.items) { expect(ids1.has(it.log_id ?? it.id)).toBe(false); // 不重叠 } await ctx.dispose(); });