/** * E2E:钱包 + 下注 + 注单查看 * * 链路: * 1. 玩家登录 → 拿钱包余额 * 2. 取当前 draw_no(公开 draw.current) * 3. POST /ticket/preview(不落库,只算钱) * 4. POST /ticket/place(落库 + 扣 frozen) * 5. GET /ticket/items/{ticket_no} 验真 * 6. 钱包余额减少(frozen 增加) * 7. 同 client_trace_id 第二次 place → 幂等回放 * * 不做结算(结算链路走 settlement service + command,e2e 单独测)。 */ import { test, expect, request as pwRequest } from '@playwright/test'; import { playerLoginViaBypass, resetE2EPlayer, fetchOpenDrawNo } from './_helper'; const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase(); const BET_AMOUNT_MINOR = 1000; test.beforeEach(async () => { await resetE2EPlayer(); }); test('玩家登录 → /wallet/balance 返回币种 + 余额', async () => { const session = await playerLoginViaBypass(); const ctx = await pwRequest.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` }, }); const r = await ctx.get('/api/v1/wallet/balance'); expect(r.ok(), `balance status=${r.status()}`).toBeTruthy(); const body = (await r.json()) as any; expect(body.data.currency_code).toBe(CURRENCY); expect(typeof body.data.balance).toBe('number'); expect(typeof body.data.available_balance).toBe('number'); expect(body.data.credit_line_mode).toBe(false); await ctx.dispose(); }); test('preview → place → 拿 ticket_no + 余额变动', async () => { const drawNo = await fetchOpenDrawNo(); const session = await playerLoginViaBypass(); const ctx = await pwRequest.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` }, }); const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any; const before = Number(bal0.data.balance); const preview = await ctx.post('/api/v1/ticket/preview', { data: { draw_id: drawNo, currency_code: CURRENCY, client_trace_id: `e2e-preview-${Date.now()}`, lines: [{ number: '1234', play_code: 'straight', amount: BET_AMOUNT_MINOR }], }, }); expect(preview.ok(), `preview status=${preview.status()}`).toBeTruthy(); const previewBody = (await preview.json()) as any; expect(previewBody.code).toBe(0); const place = await ctx.post('/api/v1/ticket/place', { data: { draw_id: drawNo, currency_code: CURRENCY, client_trace_id: `e2e-place-${Date.now()}`, lines: [{ number: '1234', play_code: 'straight', amount: BET_AMOUNT_MINOR }], }, }); expect(place.ok(), `place status=${place.status()}`).toBeTruthy(); const placeBody = (await place.json()) as any; expect(placeBody.code).toBe(0); const ticketNo = placeBody.data.items?.[0]?.ticket_no ?? placeBody.data.ticket_no; expect(ticketNo).toMatch(/^TK[0-9]+$/); const show = await ctx.get(`/api/v1/ticket/items/${ticketNo}`); expect(show.ok(), `items show status=${show.status()}`).toBeTruthy(); const showBody = (await show.json()) as any; expect(showBody.data.ticket_no).toBe(ticketNo); const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any; const afterAvailable = Number(bal1.data.available_balance); expect(afterAvailable).toBeLessThan(before); await ctx.dispose(); }); test('同 client_trace_id 第二次 place → 幂等回放,不重复扣款', async () => { const drawNo = await fetchOpenDrawNo(); const session = await playerLoginViaBypass(); const ctx = await pwRequest.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${session.access_token}` }, }); const trace = `e2e-idem-${Date.now()}`; const payload = { draw_id: drawNo, currency_code: CURRENCY, client_trace_id: trace, lines: [{ number: '5678', play_code: 'straight', amount: 500 }], }; const r1 = await ctx.post('/api/v1/ticket/place', { data: payload }); expect(r1.ok(), `place1 status=${r1.status()}`).toBeTruthy(); const body1 = (await r1.json()) as any; const ticket1 = body1.data.items?.[0]?.ticket_no ?? body1.data.ticket_no; const r2 = await ctx.post('/api/v1/ticket/place', { data: payload }); expect(r2.ok(), `place2 status=${r2.status()}`).toBeTruthy(); const body2 = (await r2.json()) as any; const ticket2 = body2.data.items?.[0]?.ticket_no ?? body2.data.ticket_no; expect(ticket1).toBe(ticket2); await ctx.dispose(); });