feat: 充值订单审计与重新申请,优化赛事展示和余额刷新
- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ 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 { expectAppErrorThrows, expectEqual, expectTrue } from './smoke-test.helpers';
|
||||
import type { SmokeTestCaseDef } from './smoke-test.cases';
|
||||
import {
|
||||
BetFlowFixtureIds,
|
||||
@@ -175,7 +175,7 @@ export function createBetFlowProbes(deps: BetFlowProbeDeps): SmokeTestCaseDef[]
|
||||
run: async () => {
|
||||
const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 50 });
|
||||
try {
|
||||
await expectThrows(
|
||||
await expectAppErrorThrows(
|
||||
'placeSingleBet',
|
||||
async () => {
|
||||
await deps.bets.placeSingleBet(
|
||||
@@ -187,7 +187,7 @@ export function createBetFlowProbes(deps: BetFlowProbeDeps): SmokeTestCaseDef[]
|
||||
`smoke-insuf-${fx.runId}`,
|
||||
);
|
||||
},
|
||||
'Insufficient balance',
|
||||
'INSUFFICIENT_BALANCE',
|
||||
);
|
||||
|
||||
const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type ScoreInput,
|
||||
} from '../../settlement/domain/settlement-calculator';
|
||||
import { resolveCashbackRateForBet } from '../cashback/cashback-rate.resolver';
|
||||
import { expectEqual, expectFalse, expectThrows, expectTrue } from './smoke-test.helpers';
|
||||
import { expectAppErrorThrows, expectEqual, expectFalse, expectTrue } from './smoke-test.helpers';
|
||||
import type { SmokeTestCaseMeta } from './smoke-test.types';
|
||||
|
||||
export type SmokeTestRunner = () => void | Promise<void>;
|
||||
@@ -547,7 +547,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
name: '低于最小投注额拒绝',
|
||||
run: async () => {
|
||||
const service = createBettingLimitsService();
|
||||
await expectThrows(
|
||||
await expectAppErrorThrows(
|
||||
'min stake',
|
||||
() =>
|
||||
service.validateBet({
|
||||
@@ -556,7 +556,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
stake: 0.5,
|
||||
potentialReturn: new Decimal(1),
|
||||
}),
|
||||
'Minimum stake is 1',
|
||||
'MIN_STAKE',
|
||||
{ stake: 0.5, minStake: 1 },
|
||||
);
|
||||
},
|
||||
@@ -567,7 +567,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
name: '超过单关最大投注额拒绝',
|
||||
run: async () => {
|
||||
const service = createBettingLimitsService();
|
||||
await expectThrows(
|
||||
await expectAppErrorThrows(
|
||||
'max stake',
|
||||
() =>
|
||||
service.validateBet({
|
||||
@@ -576,7 +576,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
stake: 60000,
|
||||
potentialReturn: new Decimal(70000),
|
||||
}),
|
||||
'Maximum stake is 50000',
|
||||
'MAX_STAKE',
|
||||
{ stake: 60000, maxStakeSingle: 50000 },
|
||||
);
|
||||
},
|
||||
@@ -587,7 +587,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
name: '超过最高派彩拒绝',
|
||||
run: async () => {
|
||||
const service = createBettingLimitsService();
|
||||
await expectThrows(
|
||||
await expectAppErrorThrows(
|
||||
'max payout',
|
||||
() =>
|
||||
service.validateBet({
|
||||
@@ -596,7 +596,7 @@ export const SMOKE_TEST_CASES: SmokeTestCaseDef[] = [
|
||||
stake: 100,
|
||||
potentialReturn: new Decimal(600000),
|
||||
}),
|
||||
'Potential return exceeds limit',
|
||||
'MAX_PAYOUT',
|
||||
{ potentialReturn: 600000, maxPayoutSingle: 500000 },
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { HttpException } from '@nestjs/common';
|
||||
import type { ApiErrorCode } from '@thebet365/shared';
|
||||
import { isCodedExceptionResponse } from '../../../shared/common/app-error';
|
||||
|
||||
export type SmokeTestStep = {
|
||||
label: string;
|
||||
input?: string;
|
||||
@@ -62,6 +66,18 @@ export function expectFalse(label: string, condition: boolean, input?: unknown,
|
||||
recordStep(label, input, 'false', condition ? failHint ?? 'true' : 'false', !condition);
|
||||
}
|
||||
|
||||
function resolveErrorCode(err: unknown): string | null {
|
||||
if (err instanceof HttpException) {
|
||||
const res = err.getResponse();
|
||||
if (isCodedExceptionResponse(res)) return res.code;
|
||||
}
|
||||
if (typeof err === 'object' && err !== null && 'response' in err) {
|
||||
const res = (err as { response: unknown }).response;
|
||||
if (isCodedExceptionResponse(res)) return res.code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function expectThrows(
|
||||
label: string,
|
||||
fn: () => void | Promise<void>,
|
||||
@@ -77,6 +93,21 @@ export async function expectThrows(
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectAppErrorThrows(
|
||||
label: string,
|
||||
fn: () => void | Promise<void>,
|
||||
code: ApiErrorCode,
|
||||
input?: unknown,
|
||||
) {
|
||||
try {
|
||||
await fn();
|
||||
recordStep(label, input, `error code ${code}`, 'no error thrown', false);
|
||||
} catch (err) {
|
||||
const actual = resolveErrorCode(err) ?? (err instanceof Error ? err.message : String(err));
|
||||
recordStep(label, input, `error code ${code}`, actual, actual === code);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatStepsForResult(steps: SmokeTestStep[]): string[] {
|
||||
return steps.map((step, index) => {
|
||||
const lines = [`${index + 1}. ${step.label}`];
|
||||
|
||||
Reference in New Issue
Block a user