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>
255 lines
9.0 KiB
TypeScript
255 lines
9.0 KiB
TypeScript
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);
|
||
}
|
||
},
|
||
},
|
||
];
|
||
}
|