账期收付改为按累计已付同步 credit_ledger,修复多笔 partial_paid 与坏账核销重复释额;新增 API E2E(占成、权限、部分收付/坏账)与 GitHub Actions e2e-api job。 Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
/**
|
||
* 开奖 → 结算 → 派彩 确定性流水线(poll + tick,避免 test.skip)。
|
||
*/
|
||
|
||
import { expect, type APIRequestContext } from '@playwright/test';
|
||
import {
|
||
buildAllResultItems,
|
||
finishDrawCooldown,
|
||
forceCloseDraw,
|
||
resolveDrawId,
|
||
tickDraws,
|
||
waitForDrawStatus,
|
||
} from '../_helper';
|
||
|
||
const CURRENCY = (process.env.LOTTERY_DEFAULT_CURRENCY ?? 'NPR').toUpperCase();
|
||
|
||
export interface DrawSettlementResult {
|
||
drawNo: string;
|
||
drawId: number;
|
||
batchId: number;
|
||
balanceBefore?: number;
|
||
balanceAfter?: number;
|
||
}
|
||
|
||
export async function runWalletDrawSettlement(params: {
|
||
adminToken: string;
|
||
playerToken: string;
|
||
drawNo: string;
|
||
betNumber?: string;
|
||
/** 公布头奖号码;默认与 betNumber 相同(必中)。传不同值可测玩家输单。 */
|
||
publishWinNumber?: string;
|
||
betAmount?: number;
|
||
balanceBefore?: number;
|
||
expectWalletIncrease?: boolean;
|
||
}): Promise<DrawSettlementResult> {
|
||
const drawNo = params.drawNo;
|
||
const betNumber = params.betNumber ?? '1234';
|
||
const publishWinNumber = params.publishWinNumber ?? betNumber;
|
||
const betAmount = params.betAmount ?? 10_000;
|
||
|
||
const player = await pwPlayerCtx(params.playerToken);
|
||
let balanceBefore: number | undefined;
|
||
if (params.expectWalletIncrease !== false) {
|
||
balanceBefore =
|
||
params.balanceBefore ??
|
||
Number((await (await player.get('/api/v1/wallet/balance')).json()).data.balance);
|
||
}
|
||
|
||
const place = await player.post('/api/v1/ticket/place', {
|
||
data: {
|
||
draw_id: drawNo,
|
||
currency_code: CURRENCY,
|
||
client_trace_id: `e2e-settle-${Date.now()}`,
|
||
lines: [{ number: betNumber, play_code: 'straight', amount: betAmount }],
|
||
},
|
||
});
|
||
expect(place.ok(), `place status=${place.status()}`).toBeTruthy();
|
||
const placeBody = (await place.json()) as any;
|
||
expect(placeBody.code).toBe(0);
|
||
await player.dispose();
|
||
|
||
await forceCloseDraw(drawNo);
|
||
await waitForDrawStatus(drawNo, ['closed', 'review', 'cooldown', 'settling', 'settled'], 15);
|
||
|
||
const admin = await pwAdminCtx(params.adminToken);
|
||
const drawId = await resolveDrawId(admin, drawNo);
|
||
|
||
const store = await admin.post(`/api/v1/admin/draws/${drawId}/result-batches`, {
|
||
data: { items: buildAllResultItems(publishWinNumber) },
|
||
});
|
||
expect(store.ok(), `store batch status=${store.status()}`).toBeTruthy();
|
||
const storeBody = (await store.json()) as any;
|
||
expect(storeBody.code).toBe(0);
|
||
const batchId: number = storeBody.data.batch.id;
|
||
|
||
const pub = await admin.post(`/api/v1/admin/draws/${drawId}/result-batches/${batchId}/publish`);
|
||
expect(pub.ok(), `publish status=${pub.status()}`).toBeTruthy();
|
||
const pubBody = (await pub.json()) as any;
|
||
expect(pubBody.code).toBe(0);
|
||
|
||
await finishDrawCooldown(drawNo);
|
||
await waitForDrawStatus(drawNo, ['settling', 'settled'], 25);
|
||
|
||
const insp = await waitForDrawStatus(drawNo, ['settled'], 25);
|
||
if (String(insp.data?.status ?? insp.status) !== 'settled') {
|
||
const settle = await admin.post(`/api/v1/admin/draws/${drawId}/settlement/run`);
|
||
const settleBody = (await settle.json()) as any;
|
||
if (settleBody.code !== 0) {
|
||
await tickDraws();
|
||
}
|
||
await waitForDrawStatus(drawNo, ['settled'], 25);
|
||
}
|
||
|
||
const list = await admin.get(`/api/v1/admin/settlement-batches?size=20`);
|
||
const lb = (await list.json()) as any;
|
||
const ourBatch = lb.data.items.find((b: any) => b.draw_id === drawId);
|
||
if (ourBatch && ourBatch.status === 'pending_review') {
|
||
await admin.post(`/api/v1/admin/settlement-batches/${ourBatch.id}/approve`, {
|
||
data: { remark: 'e2e approve' },
|
||
});
|
||
await admin.post(`/api/v1/admin/settlement-batches/${ourBatch.id}/payout`);
|
||
} else {
|
||
await tickDraws();
|
||
await waitForDrawStatus(drawNo, ['settled'], 15);
|
||
}
|
||
|
||
await admin.dispose();
|
||
|
||
let balanceAfter: number | undefined;
|
||
if (params.expectWalletIncrease !== false) {
|
||
const player2 = await pwPlayerCtx(params.playerToken);
|
||
const bal1 = (await (await player2.get('/api/v1/wallet/balance')).json()) as any;
|
||
balanceAfter = Number(bal1.data.balance);
|
||
await player2.dispose();
|
||
}
|
||
|
||
return { drawNo, drawId, batchId, balanceBefore, balanceAfter };
|
||
}
|
||
|
||
async function pwAdminCtx(token: string): Promise<APIRequestContext> {
|
||
const { request: pwRequest } = await import('@playwright/test');
|
||
return pwRequest.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||
});
|
||
}
|
||
|
||
async function pwPlayerCtx(token: string): Promise<APIRequestContext> {
|
||
const { request: pwRequest } = await import('@playwright/test');
|
||
return pwRequest.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||
});
|
||
}
|