feat: add smoke tests, agent credit ledger, and player cashback page

Introduce admin smoke-test suite with API probes, agent credit transaction history, and player cashback records; fix SmokeTestModule DI and polish admin/player UI assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 16:05:48 +08:00
parent 9c6c5e51f3
commit d5e7c8edb3
52 changed files with 3357 additions and 67 deletions

View File

@@ -0,0 +1,254 @@
import { Decimal } from '@prisma/client/runtime/library';
import type { AgentsService } from '../../agent/agents.service';
import type { BetsService } from '../../betting/bets.service';
import type { SettlementService } from '../../settlement/settlement.service';
import type { WalletService } from '../../ledger/wallet.service';
import type { PrismaService } from '../../../shared/prisma/prisma.service';
import { expectEqual, expectThrows, expectTrue } from './smoke-test.helpers';
import type { SmokeTestCaseDef } from './smoke-test.cases';
import {
BetFlowFixtureIds,
createBetFlowFixture,
teardownBetFlowFixture,
} from './smoke-test.bet-flow.fixture';
export const BET_FLOW_PROBE_COUNT = 5;
export type BetFlowProbeDeps = {
prisma: PrismaService;
wallet: WalletService;
bets: BetsService;
settlement: SettlementService;
agents: AgentsService;
};
async function confirmMatchSettlement(
deps: BetFlowProbeDeps,
fx: BetFlowFixtureIds,
score: { htHome: number; htAway: number; ftHome: number; ftAway: number },
) {
await deps.settlement.recordScore(
fx.matchId,
score.htHome,
score.htAway,
score.ftHome,
score.ftAway,
fx.operatorId,
);
const preview = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId);
await deps.settlement.confirmSettlement(preview.batch.id, fx.operatorId);
}
export function createBetFlowProbes(deps: BetFlowProbeDeps): SmokeTestCaseDef[] {
return [
{
id: 'BF001',
suite: 'bet-flow',
name: '单关赢:冻结后结算派彩',
uatRef: '8/14',
description: '下注 100@2.0 主胜,比分 2-1余额 1000→1100',
run: async () => {
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
try {
const bet = await deps.bets.placeSingleBet(
fx.playerId,
null,
fx.homeSelectionId,
fx.homeOddsVersion,
100,
`smoke-win-${fx.runId}`,
);
expectEqual('bet.status', bet.status, 'PENDING');
expectEqual('bet.stake', bet.stake.toString(), '100');
let w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available after freeze', w.availableBalance.toString(), '900');
expectEqual('frozen after freeze', w.frozenBalance.toString(), '100');
await confirmMatchSettlement(deps, fx, { htHome: 1, htAway: 0, ftHome: 2, ftAway: 1 });
const settled = await deps.prisma.bet.findUnique({ where: { id: bet.id } });
expectEqual('settled status', settled?.status, 'WON');
expectEqual('actualReturn', settled?.actualReturn.toString(), '200');
w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available after settle', w.availableBalance.toString(), '1100');
expectEqual('frozen after settle', w.frozenBalance.toString(), '0');
const txs = await deps.prisma.walletTransaction.findMany({
where: { userId: fx.playerId },
orderBy: { createdAt: 'asc' },
});
expectEqual(
'wallet tx types',
txs.map((t) => t.transactionType).join(','),
'MANUAL_DEPOSIT,BET_FREEZE,BET_SETTLE_WIN',
);
} finally {
await teardownBetFlowFixture(deps.prisma, fx);
}
},
},
{
id: 'BF002',
suite: 'bet-flow',
name: '单关输:冻结本金被消耗',
uatRef: '14',
description: '和局选项 + 2-1 比分,余额 1000→900',
run: async () => {
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
try {
const bet = await deps.bets.placeSingleBet(
fx.playerId,
null,
fx.drawSelectionId,
fx.drawOddsVersion,
100,
`smoke-lose-${fx.runId}`,
);
let w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available after freeze', w.availableBalance.toString(), '900');
expectEqual('frozen after freeze', w.frozenBalance.toString(), '100');
await confirmMatchSettlement(deps, fx, { htHome: 1, htAway: 0, ftHome: 2, ftAway: 1 });
const settled = await deps.prisma.bet.findUnique({ where: { id: bet.id } });
expectEqual('settled status', settled?.status, 'LOST');
expectEqual('actualReturn', settled?.actualReturn.toString(), '0');
w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available after settle', w.availableBalance.toString(), '900');
expectEqual('frozen after settle', w.frozenBalance.toString(), '0');
} finally {
await teardownBetFlowFixture(deps.prisma, fx);
}
},
},
{
id: 'BF003',
suite: 'bet-flow',
name: '下注幂等:相同 requestId 不重复扣款',
uatRef: '8',
description: 'userId + requestId 唯一,重复提交返回同一注单',
run: async () => {
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 500 });
try {
const requestId = `smoke-idem-${fx.runId}`;
const first = await deps.bets.placeSingleBet(
fx.playerId,
null,
fx.homeSelectionId,
fx.homeOddsVersion,
50,
requestId,
);
const second = await deps.bets.placeSingleBet(
fx.playerId,
null,
fx.homeSelectionId,
fx.homeOddsVersion,
50,
requestId,
);
expectEqual('same bet id', second.id.toString(), first.id.toString());
const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
expectEqual('bet count', count, 1);
const w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available', w.availableBalance.toString(), '450');
expectEqual('frozen', w.frozenBalance.toString(), '50');
} finally {
await teardownBetFlowFixture(deps.prisma, fx);
}
},
},
{
id: 'BF004',
suite: 'bet-flow',
name: '余额不足:拒绝下注',
uatRef: '8',
description: '可用 50 时下 100 注单应失败且无冻结',
run: async () => {
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 50 });
try {
await expectThrows(
'placeSingleBet',
async () => {
await deps.bets.placeSingleBet(
fx.playerId,
null,
fx.homeSelectionId,
fx.homeOddsVersion,
100,
`smoke-insuf-${fx.runId}`,
);
},
'Insufficient balance',
);
const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
expectEqual('bet count', count, 0);
const w = await deps.wallet.getWallet(fx.playerId);
expectEqual('available', w.availableBalance.toString(), '50');
expectEqual('frozen', w.frozenBalance.toString(), '0');
} finally {
await teardownBetFlowFixture(deps.prisma, fx);
}
},
},
{
id: 'BF005',
suite: 'bet-flow',
name: '代理额度:结算后 usedCredit 同步',
uatRef: '4/14',
description: '玩家输 100 后代理 usedCredit 1000→900',
run: async () => {
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, {
initialBalance: 1000,
withAgent: true,
});
try {
expectTrue('agent exists', !!fx.agentId);
await deps.agents.recalculateUsedCredit(fx.agentId!);
let profile = await deps.prisma.agentProfile.findUnique({
where: { userId: fx.agentId! },
});
expectEqual('usedCredit before bet', profile?.usedCredit.toString(), '1000');
await deps.bets.placeSingleBet(
fx.playerId,
fx.agentId!,
fx.drawSelectionId,
fx.drawOddsVersion,
100,
`smoke-agent-${fx.runId}`,
);
await confirmMatchSettlement(deps, fx, { htHome: 0, htAway: 0, ftHome: 2, ftAway: 1 });
await deps.agents.recalculateUsedCredit(fx.agentId!);
profile = await deps.prisma.agentProfile.findUnique({ where: { userId: fx.agentId! } });
expectEqual('usedCredit after settle', profile?.usedCredit.toString(), '900');
const w = await deps.wallet.getWallet(fx.playerId);
expectEqual('player available', w.availableBalance.toString(), '900');
expectTrue(
'directPlayerLiability matches wallet',
new Decimal(profile!.directPlayerLiability).eq(w.availableBalance.add(w.frozenBalance)),
{
liability: profile?.directPlayerLiability.toString(),
wallet: w.availableBalance.add(w.frozenBalance).toString(),
},
);
} finally {
await teardownBetFlowFixture(deps.prisma, fx);
}
},
},
];
}