Files
thebet365/apps/api/src/domains/operations/smoke-tests/smoke-test.bet-flow-probes.ts
Mars d5e7c8edb3 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>
2026-06-09 16:05:48 +08:00

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