Files
thebet365/apps/api/src/domains/operations/smoke-tests/smoke-test.bet-flow-probes.ts
Mars afb5c5437e feat: 充值订单审计与重新申请,优化赛事展示和余额刷新
- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路
- 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请
- 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等
- 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口
- 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新
- 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 14:52:05 +08:00

256 lines
9.1 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 { expectAppErrorThrows, expectEqual, 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.prisma.match.update({
where: { id: fx.matchId },
data: { status: 'CLOSED', closeTime: new Date() },
});
const preview = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId, {
htHome: score.htHome,
htAway: score.htAway,
ftHome: score.ftHome,
ftAway: score.ftAway,
});
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 expectAppErrorThrows(
'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);
}
},
},
];
}