feat: 充值订单审计与重新申请,优化赛事展示和余额刷新
- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,7 +9,7 @@ describe('CatalogArchiveService', () => {
|
||||
match: { findFirst: jest.Mock; update: jest.Mock; findMany: jest.Mock; updateMany: jest.Mock };
|
||||
league: { findFirst: jest.Mock; update: jest.Mock };
|
||||
bet: { findMany: jest.Mock };
|
||||
settlementBatch: { findFirst: jest.Mock; findMany: jest.Mock };
|
||||
settlementBatch: { findFirst: jest.Mock; findMany: jest.Mock; deleteMany: jest.Mock };
|
||||
market: { updateMany: jest.Mock };
|
||||
marketSelection: { updateMany: jest.Mock };
|
||||
entityTranslation: { findFirst: jest.Mock };
|
||||
@@ -28,7 +28,11 @@ describe('CatalogArchiveService', () => {
|
||||
},
|
||||
league: { findFirst: jest.fn(), update: jest.fn() },
|
||||
bet: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
settlementBatch: { findFirst: jest.fn().mockResolvedValue(null), findMany: jest.fn().mockResolvedValue([]) },
|
||||
settlementBatch: {
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
market: { updateMany: jest.fn() },
|
||||
marketSelection: { updateMany: jest.fn() },
|
||||
entityTranslation: { findFirst: jest.fn().mockResolvedValue(null) },
|
||||
@@ -71,6 +75,40 @@ describe('CatalogArchiveService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('archives settled match with stale preview batch when force is true', async () => {
|
||||
prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'SETTLED' });
|
||||
prisma.bet.findMany.mockResolvedValue([]);
|
||||
prisma.settlementBatch.findFirst.mockResolvedValue({ id: BigInt(99) });
|
||||
|
||||
const result = await service.archiveMatch(matchId, { force: true });
|
||||
|
||||
expect(result.matchId).toBe(matchId.toString());
|
||||
expect(prisma.settlementBatch.deleteMany).toHaveBeenCalledWith({
|
||||
where: { matchId, status: 'PREVIEW' },
|
||||
});
|
||||
expect(prisma.match.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
deletedAt: expect.any(Date),
|
||||
status: 'SETTLED',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('archives settled match without force when no warnings', async () => {
|
||||
prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'SETTLED' });
|
||||
prisma.bet.findMany.mockResolvedValue([]);
|
||||
|
||||
await service.archiveMatch(matchId, { force: false });
|
||||
|
||||
expect(prisma.match.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ status: 'SETTLED' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('archive with force rejects draft matches', async () => {
|
||||
prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'DRAFT' });
|
||||
|
||||
|
||||
@@ -70,9 +70,6 @@ export class CatalogArchiveService {
|
||||
if (match.status === 'DRAFT') {
|
||||
throw appBadRequest('MATCH_DELETE_DRAFT_ONLY');
|
||||
}
|
||||
if (match.status === 'SETTLED') {
|
||||
throw appBadRequest('ARCHIVE_BLOCKED');
|
||||
}
|
||||
const preview = await this.getMatchArchivePreview(matchId);
|
||||
if (preview.requiresForce && !opts.force) {
|
||||
throw appConflict('ARCHIVE_BLOCKED', preview);
|
||||
@@ -80,6 +77,9 @@ export class CatalogArchiveService {
|
||||
|
||||
const now = new Date();
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.settlementBatch.deleteMany({
|
||||
where: { matchId, status: 'PREVIEW' },
|
||||
});
|
||||
await tx.marketSelection.updateMany({
|
||||
where: { market: { matchId } },
|
||||
data: { status: 'CLOSED' },
|
||||
@@ -92,8 +92,7 @@ export class CatalogArchiveService {
|
||||
where: { id: matchId },
|
||||
data: {
|
||||
deletedAt: now,
|
||||
status:
|
||||
match.status === 'CANCELLED' || match.status === 'VOID' ? match.status : 'CANCELLED',
|
||||
status: TERMINAL_MATCH_STATUSES.has(match.status) ? match.status : 'CANCELLED',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1256,7 +1256,7 @@ export class MatchesService {
|
||||
homeTeamLogoUrl: m.homeTeam?.logoUrl ?? null,
|
||||
awayTeamLogoUrl: m.awayTeam?.logoUrl ?? null,
|
||||
startTime: m.startTime.toISOString(),
|
||||
isHot: m.isHot ?? false,
|
||||
isHot: m.status === 'SETTLED' ? false : (m.isHot ?? false),
|
||||
displayOrder: m.displayOrder ?? 0,
|
||||
matchName: m.matchName ?? null,
|
||||
stage: m.stage ?? null,
|
||||
|
||||
Reference in New Issue
Block a user