重构
This commit is contained in:
153
apps/api/src/domains/betting/bets.service.spec.ts
Normal file
153
apps/api/src/domains/betting/bets.service.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { BetsService } from './bets.service';
|
||||
import { expectAppError } from '../../testing/prisma-mock';
|
||||
|
||||
describe('BetsService', () => {
|
||||
const tx = {
|
||||
bet: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
marketSelection: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
};
|
||||
const prisma = {
|
||||
...tx,
|
||||
$transaction: jest.fn(async (fn: (client: typeof tx) => Promise<unknown>) => fn(tx)),
|
||||
};
|
||||
const funds = {
|
||||
freezeBet: jest.fn(),
|
||||
};
|
||||
const bettingLimits = {
|
||||
validateBet: jest.fn(),
|
||||
};
|
||||
|
||||
let service: BetsService;
|
||||
|
||||
function selection(id: bigint, oddsVersion: bigint, matchId = 88n) {
|
||||
return {
|
||||
id,
|
||||
marketId: id + 100n,
|
||||
selectionCode: id === 1n ? 'HOME' : 'AWAY',
|
||||
selectionName: `Selection ${id}`,
|
||||
nameI18n: null,
|
||||
odds: new Decimal(id === 1n ? 1.9 : 2.1),
|
||||
oddsVersion,
|
||||
status: 'OPEN',
|
||||
market: {
|
||||
matchId,
|
||||
marketType: 'FT_1X2',
|
||||
period: 'FT',
|
||||
nameI18n: null,
|
||||
lineValue: null,
|
||||
allowSingle: true,
|
||||
allowParlay: true,
|
||||
showOnPlayer: true,
|
||||
status: 'OPEN',
|
||||
match: {
|
||||
status: 'PUBLISHED',
|
||||
sportType: 'FOOTBALL',
|
||||
isOutright: false,
|
||||
startTime: new Date(Date.now() + 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new BetsService(prisma as never, funds as never, bettingLimits as never);
|
||||
tx.bet.findUnique.mockResolvedValue(null);
|
||||
tx.bet.create.mockResolvedValue({ id: 123n, betNo: 'BET-OK', selections: [] });
|
||||
bettingLimits.validateBet.mockResolvedValue(undefined);
|
||||
tx.marketSelection.findUnique.mockImplementation(({ where }: { where: { id: bigint } }) =>
|
||||
Promise.resolve(selection(where.id, where.id)),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a parlay with multiple legs from the same match', async () => {
|
||||
await expect(
|
||||
service.placeParlayBet(
|
||||
7n,
|
||||
3n,
|
||||
[
|
||||
{ selectionId: 1n, oddsVersion: 1n },
|
||||
{ selectionId: 2n, oddsVersion: 2n },
|
||||
],
|
||||
50,
|
||||
'REQ-SAME-MATCH',
|
||||
),
|
||||
).rejects.toMatchObject(expectAppError('PARLAY_SAME_MATCH_FORBIDDEN'));
|
||||
|
||||
expect(tx.bet.create).not.toHaveBeenCalled();
|
||||
expect(bettingLimits.validateBet).not.toHaveBeenCalled();
|
||||
expect(funds.freezeBet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows a parlay only when every leg is from a different match', async () => {
|
||||
tx.marketSelection.findUnique.mockImplementation(({ where }: { where: { id: bigint } }) =>
|
||||
Promise.resolve(selection(where.id, where.id, where.id === 1n ? 88n : 99n)),
|
||||
);
|
||||
|
||||
await service.placeParlayBet(
|
||||
7n,
|
||||
3n,
|
||||
[
|
||||
{ selectionId: 1n, oddsVersion: 1n },
|
||||
{ selectionId: 2n, oddsVersion: 2n },
|
||||
],
|
||||
50,
|
||||
'REQ-SAME-MATCH',
|
||||
);
|
||||
|
||||
expect(tx.bet.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
betType: 'PARLAY',
|
||||
selections: expect.objectContaining({
|
||||
create: expect.arrayContaining([
|
||||
expect.objectContaining({ matchId: 88n, selectionId: 1n }),
|
||||
expect.objectContaining({ matchId: 99n, selectionId: 2n }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(bettingLimits.validateBet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ betType: 'PARLAY', tx }),
|
||||
);
|
||||
expect(funds.freezeBet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userId: 7n, stake: expect.any(Decimal), tx }),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a single bet when the market disables single betting', async () => {
|
||||
tx.marketSelection.findUnique.mockResolvedValue({
|
||||
...selection(1n, 1n),
|
||||
market: {
|
||||
...selection(1n, 1n).market,
|
||||
allowSingle: false,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.placeSingleBet(7n, 3n, 1n, 1n, 50, 'REQ-SINGLE-DISABLED'),
|
||||
).rejects.toMatchObject(expectAppError('SINGLE_MARKET_NOT_ALLOWED'));
|
||||
|
||||
expect(tx.bet.create).not.toHaveBeenCalled();
|
||||
expect(bettingLimits.validateBet).not.toHaveBeenCalled();
|
||||
expect(funds.freezeBet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('revalidates odds version inside the placement transaction', async () => {
|
||||
tx.marketSelection.findUnique.mockResolvedValue(selection(1n, 2n));
|
||||
|
||||
await expect(
|
||||
service.placeSingleBet(7n, 3n, 1n, 1n, 50, 'REQ-ODDS-CHANGED'),
|
||||
).rejects.toMatchObject(expectAppError('ODDS_CHANGED'));
|
||||
|
||||
expect(tx.bet.create).not.toHaveBeenCalled();
|
||||
expect(funds.freezeBet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user