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:
@@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user