154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
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();
|
|
});
|
|
});
|