99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
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);
|
|
});
|
|
});
|