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) => { 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' }); }); });