Sanity Testing V1 — Financial Calculation Verification
Scope
In Scope
- Betfair exchange bets only (
bookmaker = 'betfair-ex', decimal odds) - Back and lay bet placement, settlement, reversal
- Agent hierarchy take propagation (Admin → Master Agent → Sub-Agent → Player)
- Commission calculation on exchange markets
- Exposure and available credit tracking
- Multi-bet market settlement across users
- System-wide point conservation invariants
Out of Scope
- Bifrost / Indian odds bets
- Pinnacle sportsbook bets
- B-Book routing and split-venue orders
- Frontend calculations (
calcLayLiability, betslip display) - Real Betfair API calls (PAL adapter is mocked)
- WebSocket / SSE real-time events
- Concurrent bet placement / race conditions
Test Infrastructure
- Framework: Vitest + Supertest (existing setup)
- Database: Real Prisma queries against
hannibal_testPostgreSQL - Mocked: Betfair PAL adapter (orders go straight to
accepted), Redis (existing mock) - Not Mocked: OrderService, SettlementService, TakeService, CommissionService — all run real business logic
- Location:
backend/tests/integration/sanity/ - Run command:
vitest run --config vitest.integration.config.ts
Canonical Test Values
- Odds: 3.50 (decimal), 1.50, 2.00, 10.00 (edge: high odds)
- Stakes: 100, 500, 1000
- Credit Limits: Player=1000, Sub-Agent=5000, Master Agent=10000
- Commission Rate: 2% (platform default for exchange)
Invariants (checked after every scenario)
These are the "sanity" assertions — if any fail, something is fundamentally broken.
- Point Conservation —
sum(all user balances) + treasury = constant(no points created or destroyed except mint/commission) - Take Consistency —
take = balance - creditLimitfor every entity at every level - Exposure Consistency —
exposure = sum(open order liabilities)matches what the system reports - Settlement Completeness — Settled orders have non-null
profitLoss,settlementOutcome,settledAt - No Double Settlement — Settling the same market twice doesn't double-credit any user
Scenarios
1. Back Bet Lifecycle
Setup: Player with balance=1000, creditLimit=1000
1a. Back Bet — Win
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=100, odds=3.50 | Balance decremented by 100 → 900 |
| 2 | Settle market: player's selection wins | Balance += stake × odds = 350 → 1250 |
| 3 | Verify P&L | profitLoss = +250 (= 350 - 100) |
| 4 | Verify order state | status=settled, settlementOutcome=win |
| 5 | Check invariants | Point conservation, take = 1250 - 1000 = +250 |
1b. Back Bet — Lose
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=100, odds=3.50 | Balance → 900 |
| 2 | Settle market: player's selection loses | Balance unchanged → 900 |
| 3 | Verify P&L | profitLoss = -100 |
| 4 | Check invariants | Take = 900 - 1000 = -100 |
1c. Back Bet — Void
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=100, odds=3.50 | Balance → 900 |
| 2 | Settle as void | Balance += 100 (refund) → 1000 |
| 3 | Verify P&L | profitLoss = 0 |
| 4 | Check invariants | Take = 1000 - 1000 = 0 |
2. Lay Bet Lifecycle
Setup: Player with balance=1000, creditLimit=1000
Key formula: Lay liability = stake × (odds - 1)
2a. Lay Bet — Win (selection did NOT win)
| Step | Action | Expected |
|---|---|---|
| 1 | Place lay bet: stake=100, odds=3.50 | Liability = 100 × 2.50 = 250. Balance → 750 |
| 2 | Settle: player's selection loses (lay wins) | Balance += liability + stake = 250 + 100 = 350 → 1100 |
| 3 | Verify P&L | profitLoss = +100 (won the stake) |
| 4 | Check invariants | Take = 1100 - 1000 = +100 |
2b. Lay Bet — Lose (selection won)
| Step | Action | Expected |
|---|---|---|
| 1 | Place lay bet: stake=100, odds=3.50 | Liability = 250. Balance → 750 |
| 2 | Settle: player's selection wins (lay loses) | Balance unchanged → 750 |
| 3 | Verify P&L | profitLoss = -250 (lost the liability) |
| 4 | Check invariants | Take = 750 - 1000 = -250 |
2c. Lay Bet — Void
| Step | Action | Expected |
|---|---|---|
| 1 | Place lay bet: stake=100, odds=3.50 | Liability = 250. Balance → 750 |
| 2 | Settle as void | Balance += 250 (refund) → 1000 |
| 3 | Verify P&L | profitLoss = 0 |
| 4 | Check invariants | Take = 0 |
3. Agent Hierarchy & Take Propagation
Setup:
Admin (treasury=100000)
└── Master Agent (CL=10000, balance=10000)
└── Sub-Agent (CL=5000, balance=5000)
└── Player (CL=1000, balance=1000)
3a. Player Wins — Take Propagates Up
| Step | Action | Expected |
|---|---|---|
| 1 | Player places back bet: stake=100, odds=3.50 | Player balance → 900 |
| 2 | Settle: player wins | Player balance → 1250 |
| 3 | Check player take | take = 1250 - 1000 = +250 (player profited) |
| 4 | Check sub-agent take | Sub-agent's downline AC increased by 250, take adjusts accordingly |
| 5 | Check master agent take | Master agent's downline AC reflects sub-agent change |
3b. Player Loses — Take Propagates Up
| Step | Action | Expected |
|---|---|---|
| 1 | Player places back bet: stake=100, odds=3.50 | Player balance → 900 |
| 2 | Settle: player loses | Player balance → 900 |
| 3 | Check player take | take = 900 - 1000 = -100 (player owes upline) |
| 4 | Check sub-agent take | Reflects player's loss in downline |
| 5 | Check master agent take | Reflects cascaded impact |
3c. Multiple Players Under Same Agent
| Step | Action | Expected |
|---|---|---|
| 1 | Player A places back bet: stake=200, odds=2.00 | Player A balance → 800 |
| 2 | Player B places back bet: stake=100, odds=4.00 | Player B balance → 900 |
| 3 | Settle: Player A wins, Player B loses | A: +200, B: -100 |
| 4 | Check agent take | Net = +200 - 100 = +100 reflected in agent downline |
4. Commission on Exchange Markets
Setup: Player with balance=5000, creditLimit=1000. Commission rate=2%.
4a. Net Profit — Commission Charged
| Step | Action | Expected |
|---|---|---|
| 1 | Player places back bet: stake=500, odds=2.00 | Balance → 4500 |
| 2 | Settle: player wins | Balance += 1000 → 5500. P&L = +500 |
| 3 | Commission calculation | Net market P&L = +500. Commission = 500 × 0.02 = 10 |
| 4 | Verify balance after commission | Balance = 5500 - 10 = 5490 |
| 5 | Verify commission record | Record exists with amount=10, marketId, userId |
4b. Net Loss — No Commission
| Step | Action | Expected |
|---|---|---|
| 1 | Player places back bet: stake=500, odds=2.00 | Balance → 4500 |
| 2 | Settle: player loses | Balance unchanged → 4500. P&L = -500 |
| 3 | Commission calculation | Net market P&L = -500. Commission = 0 |
| 4 | Verify no commission record | No record created |
4c. Multiple Bets in Same Market — Net P&L Commission
| Step | Action | Expected |
|---|---|---|
| 1 | Player places back bet on Outcome A: stake=300, odds=2.00 | Balance → 4700 |
| 2 | Player places back bet on Outcome B: stake=200, odds=3.00 | Balance → 4500 |
| 3 | Settle: Outcome A wins | Bet 1 wins (+300), Bet 2 loses (-200). Net = +100 |
| 4 | Commission | 100 × 0.02 = 2 |
| 5 | Final balance | 4500 + 600 + 0 - 2 = 5098 |
5. Exposure & Available Credit
Setup: Player with balance=2000, creditLimit=2000
5a. Exposure Increases with Open Bets
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=100, odds=2.00 | Exposure += 100. Balance → 1900 |
| 2 | Place lay bet: stake=100, odds=3.00 | Exposure += 200 (liability). Balance → 1700 |
| 3 | Verify total exposure | 100 + 200 = 300 |
5b. Exposure Decreases on Settlement
| Step | Action | Expected |
|---|---|---|
| 1 | Start with 2 open bets from 5a | Exposure = 300 |
| 2 | Settle first market (back bet settles) | Exposure drops by 100 → 200 |
| 3 | Settle second market (lay bet settles) | Exposure drops by 200 → 0 |
5c. Exposure Decreases on Cancellation
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=200, odds=2.50 | Exposure = 200. Balance → 1800 |
| 2 | Cancel the bet | Exposure → 0. Balance → 2000 (refund) |
6. Settlement Reversal
Setup: Player with balance=1000, creditLimit=1000
6a. Reverse a Win, Re-Settle as Lose
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=100, odds=3.50 | Balance → 900 |
| 2 | Settle: win | Balance → 1250. P&L = +250 |
| 3 | Reverse settlement | Balance → 900 (back to pre-settlement). Order status → accepted |
| 4 | Re-settle: lose | Balance → 900. P&L = -100 |
| 5 | Check invariants | Take = 900 - 1000 = -100 |
6b. Reverse with Commission — Commission Also Reversed
| Step | Action | Expected |
|---|---|---|
| 1 | Place back bet: stake=500, odds=2.00. Settle: win | Balance = 1500 - 10 (commission) = 1490 |
| 2 | Reverse settlement | Balance → 500. Commission record deleted |
| 3 | Re-settle: lose | Balance → 500. No commission (net loss) |
6c. Reverse a Lay Bet Settlement
| Step | Action | Expected |
|---|---|---|
| 1 | Place lay bet: stake=100, odds=3.50 | Liability=250. Balance → 750 |
| 2 | Settle: lay wins | Balance → 1100. P&L = +100 |
| 3 | Reverse | Balance → 750 (liability still locked). Order → accepted |
| 4 | Re-settle: lay loses | Balance → 750. P&L = -250 |
7. Multi-Bet Market Settlement
Setup: 3 players, same market, different bets
7a. Mixed Back and Lay Bets on Same Market
| Player | Bet Type | Selection | Stake | Odds |
|---|---|---|---|---|
| Player A | Back | Team 1 | 200 | 2.50 |
| Player B | Back | Team 2 | 150 | 3.00 |
| Player C | Lay | Team 1 | 100 | 2.50 |
Settle: Team 1 wins
| Player | Outcome | P&L | Balance Change |
|---|---|---|---|
| A | Win | +300 | +500 (payout) |
| B | Lose | -150 | 0 |
| C | Lose (lay) | -150 | 0 |
| Step | Action | Expected |
|---|---|---|
| 1 | Settle market with Team 1 as winner | All 3 orders settled in single pass |
| 2 | Verify each player's balance | Matches table above |
| 3 | Verify commission per player | A: 300 × 0.02 = 6. B: 0. C: 0 |
| 4 | System-wide point conservation | Total points before = total points after + commission deducted |
8. Edge Cases
8a. Minimum Odds (1.01)
- Back bet: stake=100, odds=1.01 → P&L on win = +1
- Lay bet: stake=100, odds=1.01 → liability = 1, P&L on lose = -1
8b. High Odds (1000.00)
- Back bet: stake=10, odds=1000.00 → P&L on win = +9990
- Lay bet: stake=10, odds=1000.00 → liability = 9990
8c. Insufficient Balance — Bet Rejected
- Player balance=50, attempts stake=100 → order rejected, balance unchanged
8d. Settle Already Settled Market — Idempotent
- Settle market → verify state
- Settle same market again → no changes, no errors
8e. Void After Win Reversal
- Settle as win → reverse → settle as void → verify full refund