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(); });