import { describe, expect, it } from 'vitest'; import type { Expense, Participant } from '../types'; import { calculateBalances } from './balances'; const participants: Participant[] = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }, ]; describe('calculateBalances', () => { it('nets an equal split expense among all participants', () => { const expenses: Expense[] = [ { id: 1, amountCents: 900, payerId: 1, participantIds: [1, 2, 3] }, ]; const { netBalances, settlements } = calculateBalances( participants, expenses, ); expect(netBalances).toEqual({ 1: 600, 2: -300, 3: -300 }); expect(settlements).toEqual( expect.arrayContaining([ { fromId: 2, toId: 1, amountCents: 300 }, { fromId: 3, toId: 1, amountCents: 300 }, ]), ); expect(settlements).toHaveLength(2); }); it('handles an expense split among a subset of participants', () => { const expenses: Expense[] = [ { id: 1, amountCents: 1000, payerId: 2, participantIds: [2, 3] }, ]; const { netBalances } = calculateBalances(participants, expenses); expect(netBalances).toEqual({ 1: 0, 2: 500, 3: -500 }); }); it('produces a zero-sum, fully-settled group when expenses cancel out', () => { const expenses: Expense[] = [ { id: 1, amountCents: 900, payerId: 1, participantIds: [1, 2, 3] }, { id: 2, amountCents: 900, payerId: 2, participantIds: [1, 2, 3] }, { id: 3, amountCents: 900, payerId: 3, participantIds: [1, 2, 3] }, ]; const { netBalances, settlements } = calculateBalances( participants, expenses, ); expect(Object.values(netBalances).every((v) => v === 0)).toBe(true); expect(settlements).toEqual([]); }); it('folds in a settlement payment recorded as a one-person split expense', () => { const expenses: Expense[] = [ { id: 1, amountCents: 900, payerId: 1, participantIds: [1, 2, 3] }, // Bob (2) settles 300 with Alice (1): payerId is who pays, the sole // participant is who receives it. { id: 2, amountCents: 300, payerId: 2, participantIds: [1] }, ]; const { netBalances, settlements } = calculateBalances( participants, expenses, ); expect(netBalances).toEqual({ 1: 300, 2: 0, 3: -300 }); expect(settlements).toEqual([{ fromId: 3, toId: 1, amountCents: 300 }]); }); it('handles a single-participant Pot (self-paid expense nets to zero)', () => { const solo: Participant[] = [{ id: 1, name: 'Alice' }]; const expenses: Expense[] = [ { id: 1, amountCents: 500, payerId: 1, participantIds: [1] }, ]; const { netBalances, settlements } = calculateBalances(solo, expenses); expect(netBalances).toEqual({ 1: 0 }); expect(settlements).toEqual([]); }); it('produces a minimal number of settlements for a multi-person scenario', () => { const four: Participant[] = [ { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' }, { id: 4, name: 'D' }, ]; // A pays 400 split 4 ways (each owes 100), B pays 800 split 4 ways (each owes 200). const expenses: Expense[] = [ { id: 1, amountCents: 400, payerId: 1, participantIds: [1, 2, 3, 4] }, { id: 2, amountCents: 800, payerId: 2, participantIds: [1, 2, 3, 4] }, ]; const { netBalances, settlements } = calculateBalances(four, expenses); // A: +400-100-200=100, B: +800-100-200=500, C: -300, D: -300 expect(netBalances).toEqual({ 1: 100, 2: 500, 3: -300, 4: -300 }); // 2 creditors, 2 debtors -> minimal settlement count is at most 3 (n-1 participants with nonzero balance) expect(settlements.length).toBeLessThanOrEqual(3); const total = settlements.reduce((sum, s) => sum + s.amountCents, 0); expect(total).toBe(600); }); });