File size: 4,981 Bytes
0545b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa363c5
 
 
 
 
0545b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { describe, expect, it } from 'vitest';
import { buildExpenseBody, emptyExpenseForm, expenseToFormValues, readExpenseForm } from '../../../src/lib/server/forms/expense-form';
import type { Expense } from '../../../src/lib/domain/types';

const members = ['u_a', 'u_b', 'u_c'];
const fd = (entries: [string, string][]) => {
	const f = new FormData();
	for (const [k, v] of entries) f.append(k, v);
	return f;
};

describe('readExpenseForm', () => {
	it('reads all fields, keeping only known participants', () => {
		const values = readExpenseForm(
			fd([['description', ' Pizza '], ['amount', '30'], ['date', '2026-08-01'], ['paidBy', 'u_a'], ['mode', 'percent'],
				['participants', 'u_a'], ['participants', 'u_b'], ['participants', 'u_zzz'], ['pct_u_a', '25'], ['pct_u_b', '75'], ['exact_u_a', '1'], ['notes', 'n'], ['removePhoto', 'on']]),
			members
		);
		expect(values).toEqual({
			description: 'Pizza', amount: '30', date: '2026-08-01', paidBy: 'u_a', mode: 'percent', participants: ['u_a', 'u_b'],
			pct: { u_a: '25', u_b: '75' }, exact: { u_a: '1' }, notes: 'n', removePhoto: true
		});
	});

	it('defaults unknown mode to equal', () => {
		expect(readExpenseForm(fd([['mode', 'weird']]), members).mode).toBe('equal');
	});
});

describe('buildExpenseBody', () => {
	const base = { ...emptyExpenseForm('u_a', members, '2026-08-01'), description: 'Pizza', amount: '30' };
	const opts = { activeMemberIds: members, currency: 'USD' };

	it('builds an equal split', () => {
		const r = buildExpenseBody({ ...base, participants: ['u_a', 'u_b'] }, opts);
		expect(r).toEqual({
			ok: true,
			body: { description: 'Pizza', amountCents: 3000, currency: 'USD', date: '2026-08-01', paidBy: 'u_a', split: { mode: 'equal', participants: ['u_a', 'u_b'] }, shares: { u_a: 1500, u_b: 1500 } }
		});
	});

	it('builds percent and exact splits from the participant inputs', () => {
		const pct = buildExpenseBody({ ...base, mode: 'percent', participants: ['u_a', 'u_b'], pct: { u_a: '25', u_b: '75', u_c: '100' } }, opts);
		expect(pct.ok && pct.body.split).toEqual({ mode: 'percent', bp: { u_a: 2500, u_b: 7500 } });
		expect(pct.ok && pct.body.shares).toEqual({ u_a: 750, u_b: 2250 });
		const exact = buildExpenseBody({ ...base, mode: 'exact', participants: ['u_a', 'u_b'], exact: { u_a: '10', u_b: '20' } }, opts);
		expect(exact.ok && exact.body.shares).toEqual({ u_a: 1000, u_b: 2000 });
	});

	it('includes notes only when non-empty', () => {
		const r = buildExpenseBody({ ...base, notes: '  ' }, opts);
		expect(r.ok && 'notes' in r.body).toBe(false);
		const r2 = buildExpenseBody({ ...base, notes: 'hi' }, opts);
		expect(r2.ok && r2.body.notes).toBe('hi');
	});

	it('trims the description before storing it', () => {
		const r = buildExpenseBody({ ...base, description: '   Pizza  ' }, opts);
		expect(r.ok && r.body.description).toBe('Pizza');
	});

	it('reports field errors', () => {
		const errs = (v: Partial<typeof base>) => {
			const r = buildExpenseBody({ ...base, ...v }, opts);
			return r.ok ? {} : r.errors;
		};
		expect(errs({ description: '' })).toHaveProperty('description');
		expect(errs({ amount: 'abc' })).toHaveProperty('amount');
		expect(errs({ amount: '0' })).toHaveProperty('amount');
		expect(errs({ date: '2026-02-30' })).toHaveProperty('date');
		expect(errs({ paidBy: 'u_admin' })).toHaveProperty('paidBy');
		expect(errs({ participants: [] })).toHaveProperty('participants');
		expect(errs({ participants: ['u_a', 'u_ghost'] })).toHaveProperty('participants');
		expect(errs({ mode: 'percent', pct: { u_a: '50' } })).toHaveProperty('shares');
		expect(errs({ mode: 'percent', pct: { u_a: '50', u_b: '30', u_c: '30' } })).toHaveProperty('shares');
		expect(errs({ mode: 'exact', exact: { u_a: '10', u_b: '10', u_c: '5' } })).toHaveProperty('shares');
		expect(errs({ mode: 'exact', exact: { u_a: 'x', u_b: '10', u_c: '10' } })).toHaveProperty('shares');
	});

	it('rejects a payer who is not an active member (photo/notes untouched)', () => {
		const r = buildExpenseBody({ ...base, paidBy: 'u_c' }, { ...opts, activeMemberIds: ['u_a', 'u_b'] });
		expect(r.ok).toBe(false);
	});
});

describe('expenseToFormValues', () => {
	it('maps every split mode back to inputs', () => {
		const audit = { createdBy: 'u_a', createdAt: 'x', updatedBy: 'u_a', updatedAt: 'x' };
		const e: Expense = { id: 'e_1', version: 1, description: 'D', amountCents: 1234, currency: 'USD', date: '2026-08-01', paidBy: 'u_b', split: { mode: 'percent', bp: { u_a: 3333, u_b: 6667 } }, shares: { u_a: 411, u_b: 823 }, notes: 'n', ...audit };
		expect(expenseToFormValues(e)).toEqual({ description: 'D', amount: '12.34', date: '2026-08-01', paidBy: 'u_b', mode: 'percent', participants: ['u_a', 'u_b'], pct: { u_a: '33.33', u_b: '66.67' }, exact: {}, notes: 'n', removePhoto: false });
		const ex: Expense = { ...e, split: { mode: 'exact', cents: { u_a: 34, u_b: 1200 } } };
		expect(expenseToFormValues(ex).exact).toEqual({ u_a: '0.34', u_b: '12.00' });
	});
});