feat: 充值订单审计与重新申请,优化赛事展示和余额刷新
- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,9 +7,13 @@ describe('DepositService', () => {
|
||||
$queryRaw: jest.fn(),
|
||||
depositOrder: {
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
paymentMethod: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
bet: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
@@ -19,6 +23,9 @@ describe('DepositService', () => {
|
||||
agentProfile: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
depositOrderAuditLog: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
...tx,
|
||||
@@ -65,6 +72,7 @@ describe('DepositService', () => {
|
||||
reviewedAt: null,
|
||||
screenshotUrl: '/uploads/a.png',
|
||||
});
|
||||
tx.depositOrderAuditLog.create.mockResolvedValue({ id: 501n });
|
||||
tx.user.findFirst.mockResolvedValue({ parentId: 3n });
|
||||
tx.agentProfile.findUnique.mockResolvedValue({
|
||||
creditLimit: new Decimal(1000),
|
||||
@@ -89,7 +97,7 @@ describe('DepositService', () => {
|
||||
remark: 'Bank receipt checked',
|
||||
referenceId: 'DEP-1',
|
||||
transactionType: 'PLAYER_DEPOSIT',
|
||||
businessKey: 'deposit:DEP-1:approve',
|
||||
businessKey: 'deposit:DEP-1:approve:501',
|
||||
tx,
|
||||
}),
|
||||
);
|
||||
@@ -98,6 +106,54 @@ describe('DepositService', () => {
|
||||
expect(credit.recalculateUsedCredit).toHaveBeenNthCalledWith(2, 3n, tx);
|
||||
});
|
||||
|
||||
it('uses a fresh business key when re-approving after revoke', async () => {
|
||||
const reviewedAt = new Date('2026-06-15T14:08:48.000Z');
|
||||
tx.depositOrder.findUnique.mockResolvedValue({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
amount: new Decimal(2),
|
||||
approvedAmount: null,
|
||||
status: 'PENDING',
|
||||
reviewedAt: null,
|
||||
screenshotUrl: '/uploads/a.png',
|
||||
});
|
||||
tx.depositOrderAuditLog.create.mockResolvedValueOnce({ id: 601n });
|
||||
|
||||
await service.approveDepositOrder(1n, 2n, 2, 'Second approval');
|
||||
|
||||
expect(funds.deposit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
amount: new Decimal(2),
|
||||
businessKey: 'deposit:DEP-1:approve:601',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses approval cycle key when revoking a funded deposit for re-review', async () => {
|
||||
const reviewedAt = new Date('2026-06-15T14:08:48.000Z');
|
||||
tx.depositOrder.findUnique.mockResolvedValue({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
amount: new Decimal(21),
|
||||
approvedAmount: new Decimal(21),
|
||||
status: 'APPROVED',
|
||||
reviewedAt,
|
||||
screenshotUrl: '/uploads/a.png',
|
||||
});
|
||||
tx.depositOrderAuditLog.create.mockResolvedValue({ id: 701n });
|
||||
|
||||
await service.reopenDepositOrderForReview(1n, 2n);
|
||||
|
||||
expect(funds.withdraw).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
amount: new Decimal(21),
|
||||
businessKey: `deposit:DEP-1:reopen-reverse:${reviewedAt.getTime()}`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks approved deposit revoke when bets exist after approval', async () => {
|
||||
tx.bet.findMany.mockResolvedValue([{ id: 99n }]);
|
||||
|
||||
@@ -116,4 +172,106 @@ describe('DepositService', () => {
|
||||
|
||||
expect(tx.depositOrder.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reapplies a rejected deposit on the same order for the player', async () => {
|
||||
tx.depositOrder.findUnique
|
||||
.mockResolvedValueOnce({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
paymentMethodId: 5n,
|
||||
methodType: 'BANK',
|
||||
amount: new Decimal(11),
|
||||
approvedAmount: null,
|
||||
status: 'REJECTED',
|
||||
reviewedAt: new Date(),
|
||||
rejectReason: 'Screenshot unclear',
|
||||
remark: 'Screenshot unclear',
|
||||
screenshotUrl: '/uploads/deposits/old.png',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
amount: new Decimal(11),
|
||||
status: 'PENDING',
|
||||
createdAt: new Date(),
|
||||
});
|
||||
tx.depositOrder.findFirst.mockResolvedValue(null);
|
||||
tx.paymentMethod.findUnique.mockResolvedValue({
|
||||
id: 5n,
|
||||
methodType: 'BANK',
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const result = await service.reapplyDepositOrder(
|
||||
7n,
|
||||
1n,
|
||||
'/uploads/deposits/new.png',
|
||||
11,
|
||||
);
|
||||
|
||||
expect(tx.depositOrder.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 1n },
|
||||
data: expect.objectContaining({
|
||||
status: 'PENDING',
|
||||
screenshotUrl: '/uploads/deposits/new.png',
|
||||
rejectReason: null,
|
||||
remark: null,
|
||||
reviewerId: null,
|
||||
reviewedAt: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(tx.depositOrderAuditLog.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
action: 'REOPENED',
|
||||
actorId: 7n,
|
||||
actorType: 'PLAYER',
|
||||
statusBefore: 'REJECTED',
|
||||
statusAfter: 'PENDING',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result?.orderNo).toBe('DEP-1');
|
||||
});
|
||||
|
||||
it('blocks reapply when player already has another pending deposit', async () => {
|
||||
tx.depositOrder.findUnique.mockResolvedValue({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
paymentMethodId: 5n,
|
||||
methodType: 'BANK',
|
||||
amount: new Decimal(11),
|
||||
status: 'REJECTED',
|
||||
screenshotUrl: '/uploads/deposits/old.png',
|
||||
});
|
||||
tx.depositOrder.findFirst.mockResolvedValue({ id: 99n });
|
||||
|
||||
await expect(
|
||||
service.reapplyDepositOrder(7n, 1n, '/uploads/deposits/new.png'),
|
||||
).rejects.toMatchObject(expectAppError('DEPOSIT_PENDING_ORDER_EXISTS'));
|
||||
|
||||
expect(tx.depositOrder.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks reapply for non-rejected deposit orders', async () => {
|
||||
tx.depositOrder.findUnique.mockResolvedValue({
|
||||
id: 1n,
|
||||
orderNo: 'DEP-1',
|
||||
playerId: 7n,
|
||||
paymentMethodId: 5n,
|
||||
methodType: 'BANK',
|
||||
amount: new Decimal(11),
|
||||
status: 'PENDING',
|
||||
screenshotUrl: '/uploads/deposits/old.png',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.reapplyDepositOrder(7n, 1n, '/uploads/deposits/new.png'),
|
||||
).rejects.toMatchObject(expectAppError('ORDER_NOT_REJECTED'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user