Files
lotteryLaravel/e2e/tests/api/05.wallet-transfer.spec.ts
kang 3f29229499
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
fix: 部分收付累计释额并扩展结算 E2E 与 CI
账期收付改为按累计已付同步 credit_ledger,修复多笔 partial_paid 与坏账核销重复释额;新增 API E2E(占成、权限、部分收付/坏账)与 GitHub Actions e2e-api job。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 09:48:01 +08:00

173 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* E2E玩家钱包 transfer-in / transfer-out
*
* 链路:
* 1. transfer-in: 主站扣款 → 彩票钱包加款e2e 走 stub秒成功
* 2. transfer-out: 彩票钱包扣款 → 主站加款(同样 stub 秒成功)
* 3. 余额一致性transfer-in 后 balance 增加、available_balance 增加
* 4. 幂等:同 idempotent_key 第二次返回同 transfer_no
* 5. 余额不足 1001把 balance 改到 0 → transfer-out → 1001
* 6. 幂等冲突 1010同 idempotent_key 第二次 amount 不同 → 1010
*
* 不覆盖(需外部主站 mock 或改配置才能跑):
* - 主站失败 1009需真主站返 5xx
* - 主站超时 504/408 → 1002 pending_reconcile需真主站返 timeout
* - 转入关 1004需 .env 关 transfer_in_enabled跑前要改
*/
import { test, expect, request as pwRequest } from '@playwright/test';
import { playerLoginViaBypass, resetE2EPlayer, e2e } from './_helper';
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
const IN_AMOUNT = 50_000; // 转入 50 元 NPR
test.beforeEach(async () => {
await resetE2EPlayer();
});
async function ctxOf(token: string) {
return pwRequest.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
});
}
test('transfer-in: 加款 + 余额增加 + lottery_balance_after 一致', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
const before = Number(bal0.data.balance);
const r = await ctx.post('/api/v1/wallet/transfer-in', {
data: {
amount: IN_AMOUNT,
currency: CURRENCY,
idempotent_key: `e2e-tin-${Date.now()}`,
},
});
expect(r.ok(), `transfer-in status=${r.status()}`).toBeTruthy();
const body = (await r.json()) as any;
expect(body.code).toBe(0);
expect(body.data.transfer_no).toMatch(/^T[IO]_[a-z0-9]+$/);
expect(Number(body.data.amount)).toBe(IN_AMOUNT);
expect(body.data.currency_code).toBe(CURRENCY);
// lottery_balance_after = before + IN_AMOUNT
expect(Number(body.data.lottery_balance_after)).toBe(before + IN_AMOUNT);
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
expect(Number(bal1.data.balance)).toBe(before + IN_AMOUNT);
expect(Number(bal1.data.available_balance)).toBe(before + IN_AMOUNT);
await ctx.dispose();
});
test('transfer-out: 扣款 + 余额减少', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const bal0 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
const before = Number(bal0.data.balance);
const r = await ctx.post('/api/v1/wallet/transfer-out', {
data: {
amount: IN_AMOUNT,
currency: CURRENCY,
idempotent_key: `e2e-tout-${Date.now()}`,
},
});
expect(r.ok(), `transfer-out status=${r.status()}`).toBeTruthy();
const body = (await r.json()) as any;
expect(body.code).toBe(0);
expect(Number(body.data.amount)).toBe(IN_AMOUNT);
expect(Number(body.data.lottery_balance_after)).toBe(before - IN_AMOUNT);
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
expect(Number(bal1.data.balance)).toBe(before - IN_AMOUNT);
await ctx.dispose();
});
test('transfer-in 幂等:同 key 第二次返回同 transfer_no + 不重复加款', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const key = `e2e-tin-idem-${Date.now()}`;
const payload = { amount: IN_AMOUNT, currency: CURRENCY, idempotent_key: key };
const r1 = await ctx.post('/api/v1/wallet/transfer-in', { data: payload });
const b1 = (await r1.json()) as any;
const t1 = b1.data.transfer_no;
const bal1 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
const after1 = Number(bal1.data.balance);
const r2 = await ctx.post('/api/v1/wallet/transfer-in', { data: payload });
const b2 = (await r2.json()) as any;
expect(b2.data.transfer_no).toBe(t1);
const bal2 = (await (await ctx.get('/api/v1/wallet/balance')).json()) as any;
expect(Number(bal2.data.balance)).toBe(after1); // 余额没再变
await ctx.dispose();
});
test('transfer-in 幂等冲突 1010同 key 第二次 amount 不同', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const key = `e2e-tin-conflict-${Date.now()}`;
const r1 = await ctx.post('/api/v1/wallet/transfer-in', {
data: { amount: IN_AMOUNT, currency: CURRENCY, idempotent_key: key },
});
expect(r1.ok()).toBeTruthy();
const r2 = await ctx.post('/api/v1/wallet/transfer-in', {
data: { amount: IN_AMOUNT + 100, currency: CURRENCY, idempotent_key: key },
});
// 业务失败HTTP 200code=1010
const b2 = (await r2.json()) as any;
expect(Number(b2.code)).toBe(1010);
await ctx.dispose();
});
test('transfer-out 余额不足 1001把余额改到 0 后转出', async () => {
// 把 balance 改到 0
await e2e('POST', '/player/set-balance', { balance: 0, currency: CURRENCY });
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const r = await ctx.post('/api/v1/wallet/transfer-out', {
data: {
amount: 100,
currency: CURRENCY,
idempotent_key: `e2e-tout-empty-${Date.now()}`,
},
});
const body = (await r.json()) as any;
expect(Number(body.code)).toBe(1001);
await ctx.dispose();
});
test('transfer-in 负数金额 → 422 校验失败', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const r = await ctx.post('/api/v1/wallet/transfer-in', {
data: {
amount: -100,
currency: CURRENCY,
idempotent_key: `e2e-tin-neg-${Date.now()}`,
},
});
expect([422, 400]).toContain(r.status());
await ctx.dispose();
});
test('transfer-in 缺 idempotent_key → 422 校验失败', async () => {
const session = await playerLoginViaBypass();
const ctx = await ctxOf(session.access_token);
const r = await ctx.post('/api/v1/wallet/transfer-in', {
data: { amount: IN_AMOUNT, currency: CURRENCY },
});
expect([422, 400]).toContain(r.status());
await ctx.dispose();
});