ROIBot / test /db.test.js
Codex
Allow settled bet metadata edits
7695308
import test from 'node:test';
import assert from 'node:assert/strict';
import { newDb } from 'pg-mem';
import { BetStore } from '../src/db.js';
async function createStore() {
const db = newDb();
const { Pool } = db.adapters.createPg();
const pool = new Pool();
const store = new BetStore('postgresql://test', { pool });
await store.initialize();
return store;
}
test('tracks bet numbering independently per user', async () => {
const store = await createStore();
await store.createBet({ id: 'user-a', username: 'a', displayName: 'A' }, {
book: 'FanDuel',
sport: 'NBA',
oddsInput: '-110',
normalizedDecimalOdds: 1.9091,
prop: 'Knicks ML',
stake: 25,
rawInput: 'raw',
});
await store.createBet({ id: 'user-b', username: 'b', displayName: 'B' }, {
book: 'DraftKings',
sport: 'NBA',
oddsInput: '+120',
normalizedDecimalOdds: 2.2,
prop: 'Lakers ML',
stake: 10,
rawInput: 'raw',
});
assert.equal((await store.findBet('user-a', 1)).betNumber, 1);
assert.equal((await store.findBet('user-b', 1)).betNumber, 1);
await store.close();
});
test('calculates roi excluding void bets from settled stake', async () => {
const store = await createStore();
const user = { id: 'user-a', username: 'a', displayName: 'A' };
await store.createBet(user, {
book: 'FanDuel',
sport: 'NBA',
oddsInput: '-110',
normalizedDecimalOdds: 1.9091,
prop: 'Knicks ML',
stake: 25,
rawInput: 'raw',
});
await store.createBet(user, {
book: 'DraftKings',
sport: 'NBA',
oddsInput: '+150',
normalizedDecimalOdds: 2.5,
prop: 'Lakers ML',
stake: 10,
rawInput: 'raw',
});
await store.createBet(user, {
book: 'Caesars',
sport: 'NBA',
oddsInput: '-105',
normalizedDecimalOdds: 1.9524,
prop: 'Celtics ML',
stake: 5,
rawInput: 'raw',
});
await store.resolveBet(user.id, 1, 'win');
await store.resolveBet(user.id, 2, 'loss');
await store.resolveBet(user.id, 3, 'void');
const summary = await store.getUserSummary(user.id);
assert.equal(summary.wins, 1);
assert.equal(summary.losses, 1);
assert.equal(summary.voids, 1);
assert.equal(summary.settledStake, 35);
assert.equal(summary.netProfit.toFixed(2), '12.73');
await store.close();
});
test('returns sportsbook breakdown with roi and win rate', async () => {
const store = await createStore();
const user = { id: 'user-books', username: 'books', displayName: 'Books' };
await store.createBet(user, {
book: 'FanDuel',
sport: 'MLB',
oddsInput: '+100',
normalizedDecimalOdds: 2,
prop: 'Bet A',
stake: 10,
rawInput: 'raw',
});
await store.createBet(user, {
book: 'FanDuel',
sport: 'MLB',
oddsInput: '-110',
normalizedDecimalOdds: 1.9091,
prop: 'Bet B',
stake: 20,
rawInput: 'raw',
});
await store.createBet(user, {
book: 'DraftKings',
sport: 'NBA',
oddsInput: '+150',
normalizedDecimalOdds: 2.5,
prop: 'Bet C',
stake: 10,
rawInput: 'raw',
});
await store.resolveBet(user.id, 1, 'win');
await store.resolveBet(user.id, 2, 'loss');
await store.resolveBet(user.id, 3, 'win');
const rows = await store.getBookBreakdown(user.id);
const draftKings = rows.find((row) => row.book === 'DraftKings');
const fanDuel = rows.find((row) => row.book === 'FanDuel');
assert.equal(rows.length, 2);
assert.equal(draftKings.winRatePercent, 100);
assert.equal(draftKings.roiPercent, 150);
assert.equal(fanDuel.wins, 1);
assert.equal(fanDuel.losses, 1);
assert.equal(fanDuel.totalBets, 2);
await store.close();
});
test('soft delete removes a bet from active summaries', async () => {
const store = await createStore();
const user = { id: 'user-delete', username: 'delete', displayName: 'Delete' };
await store.createBet(user, {
book: 'FanDuel',
sport: 'MLB',
oddsInput: '+100',
normalizedDecimalOdds: 2,
prop: 'Bet A',
stake: 10,
rawInput: 'raw',
});
await store.softDeleteBet(user.id, 1, 'mistake');
const summary = await store.getUserSummary(user.id);
const deletedBet = await store.findBet(user.id, 1);
assert.equal(summary.totalBets, 0);
assert.equal(deletedBet.deletedReason, 'mistake');
await store.close();
});
test('bankroll settings update user profile and units', async () => {
const store = await createStore();
const user = { id: 'user-bankroll', username: 'bankroll', displayName: 'Bankroll' };
await store.updateUserPerformanceConfig(user, {
startingBankroll: 500,
unitSize: 25,
});
await store.createBet(user, {
book: 'BetMGM',
sport: 'NFL',
oddsInput: '+150',
normalizedDecimalOdds: 2.5,
prop: 'Bet A',
stake: 50,
rawInput: 'raw',
});
const profile = await store.getUserProfile(user.id);
const bet = await store.findBet(user.id, 1);
assert.equal(profile.startingBankroll, 500);
assert.equal(profile.unitSize, 25);
assert.equal(bet.unitsValue, 2);
await store.close();
});
test('returns sport breakdown with roi and win rate', async () => {
const store = await createStore();
const user = { id: 'user-sports', username: 'sports', displayName: 'Sports' };
await store.createBet(user, {
book: 'FanDuel',
sport: 'MLB',
oddsInput: '+100',
normalizedDecimalOdds: 2,
prop: 'Bet A',
stake: 10,
rawInput: 'raw',
});
await store.createBet(user, {
book: 'DraftKings',
sport: 'NFL',
oddsInput: '-110',
normalizedDecimalOdds: 1.9091,
prop: 'Bet B',
stake: 20,
rawInput: 'raw',
});
await store.resolveBet(user.id, 1, 'win');
await store.resolveBet(user.id, 2, 'loss');
const rows = await store.getSportBreakdown(user.id);
const mlb = rows.find((row) => row.label === 'MLB');
const nfl = rows.find((row) => row.label === 'NFL');
assert.equal(rows.length, 2);
assert.equal(mlb.roiPercent, 100);
assert.equal(nfl.losses, 1);
await store.close();
});
test('allows settled bets to update metadata without changing financial fields', async () => {
const store = await createStore();
const user = { id: 'user-edit-settled', username: 'edit', displayName: 'Edit' };
await store.createBet(user, {
book: 'FanDuel',
sport: 'Other',
oddsInput: '+100',
normalizedDecimalOdds: 2,
prop: 'Bet A',
stake: 10,
rawInput: 'raw',
});
await store.resolveBet(user.id, 1, 'win');
const updated = await store.updateBet(user.id, 1, {
sport: 'MLB',
rawInput: 'book: FanDuel\nsport: MLB\nprop: Bet A\nodds: +100\nstake: 10',
});
assert.equal(updated.type, 'updated');
assert.equal(updated.bet.sport, 'MLB');
assert.equal(updated.bet.profitLoss, 10);
const locked = await store.updateBet(user.id, 1, {
stake: 25,
});
assert.equal(locked.type, 'financial_locked');
await store.close();
});