- The owner's claimant key is trimmed and lower-cased, the same rule the payout RPC applies, so "Jakob" and "jakob " are one person with one Att göra row and one exact-amount match. - The inbox pages through every registered claim (fetchAllRows) before pairing, so a long backlog can never understate a person's debt. - A foreign receipt's VAT field is locked at 0 and 0 is what is submitted. - Transport failures in the one-click and picker confirms show the destructive toast instead of failing silently. - The open-claims flag is set only after the stale-fetch guard, and a payout match decrements the inbox count like every other row exit. - Test: reset the live-link mock before the bank_line junction case. - Wording: "Återbetalning av utlägg". Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
groupExpenseClaimsByPerson,
|
|
matchTransactionsToExpensePayouts,
|
|
} from '../expense-payout-candidates'
|
|
|
|
const anna = { key: 'emp-1', employee_id: 'emp-1', claimant_name: 'Anna Berg', liability_account: '2820', claim_count: 2, claim_ids: ['c2', 'c3'], total_sek: 1596, oldest_expense_date: '2026-09-02' }
|
|
const owner = { key: 'owner:Jakob', employee_id: null, claimant_name: 'Jakob', liability_account: '2893', claim_count: 1, claim_ids: ['c1'], total_sek: 1240, oldest_expense_date: '2026-09-03' }
|
|
|
|
describe('groupExpenseClaimsByPerson', () => {
|
|
it('sums per person in öre-safe arithmetic and keeps claim ids in date order', () => {
|
|
const out = groupExpenseClaimsByPerson([
|
|
{ id: 'a', employee_id: null, claimant_name: 'Jakob', liability_account: '2893', amount_sek: '0.1', expense_date: '2026-09-01' },
|
|
{ id: 'b', employee_id: null, claimant_name: 'Jakob', liability_account: '2893', amount_sek: 0.2, expense_date: '2026-09-02' },
|
|
])
|
|
expect(out).toHaveLength(1)
|
|
expect(out[0].total_sek).toBe(0.3)
|
|
expect(out[0].claim_ids).toEqual(['a', 'b'])
|
|
})
|
|
|
|
it('treats "Jakob" and "jakob " as one owner, the way the payout RPC does', () => {
|
|
const out = groupExpenseClaimsByPerson([
|
|
{ id: 'a', employee_id: null, claimant_name: 'Jakob', liability_account: '2893', amount_sek: 100, expense_date: '2026-09-01' },
|
|
{ id: 'b', employee_id: null, claimant_name: 'jakob ', liability_account: '2893', amount_sek: 50, expense_date: '2026-09-02' },
|
|
])
|
|
expect(out).toHaveLength(1)
|
|
expect(out[0]).toMatchObject({ key: 'owner:jakob', claimant_name: 'Jakob', total_sek: 150, claim_ids: ['a', 'b'] })
|
|
})
|
|
|
|
it('leaves an enskild firma owner out: egen insättning is not a debt', () => {
|
|
const out = groupExpenseClaimsByPerson([
|
|
{ id: 'a', employee_id: null, claimant_name: 'Sara', liability_account: '2018', amount_sek: 500, expense_date: '2026-09-01' },
|
|
{ id: 'b', employee_id: 'emp-1', claimant_name: 'Anna Berg', liability_account: '2820', amount_sek: 200, expense_date: '2026-09-02' },
|
|
])
|
|
expect(out.map((p) => p.key)).toEqual(['emp-1'])
|
|
})
|
|
})
|
|
|
|
describe('matchTransactionsToExpensePayouts', () => {
|
|
it('pairs an SEK outflow with the one person owed exactly that amount', () => {
|
|
const m = matchTransactionsToExpensePayouts(
|
|
[{ id: 'tx-1', amount: -1596, currency: 'SEK', is_business: null, journal_entry_id: null }],
|
|
[anna, owner],
|
|
)
|
|
expect(m.get('tx-1')?.person.key).toBe('emp-1')
|
|
})
|
|
|
|
it('ignores inflows, booked rows, foreign currency and near misses', () => {
|
|
const m = matchTransactionsToExpensePayouts(
|
|
[
|
|
{ id: 'in', amount: 1596, currency: 'SEK', is_business: null, journal_entry_id: null },
|
|
{ id: 'booked', amount: -1596, currency: 'SEK', is_business: true, journal_entry_id: 'je' },
|
|
{ id: 'eur', amount: -1596, currency: 'EUR', is_business: null, journal_entry_id: null },
|
|
{ id: 'near', amount: -1596.01, currency: 'SEK', is_business: null, journal_entry_id: null },
|
|
],
|
|
[anna],
|
|
)
|
|
expect(m.size).toBe(0)
|
|
})
|
|
|
|
it('skips a total two people share: the amount alone cannot say who', () => {
|
|
const twin = { ...owner, key: 'owner:Emil', claimant_name: 'Emil', total_sek: 1596 }
|
|
const m = matchTransactionsToExpensePayouts(
|
|
[{ id: 'tx-1', amount: -1596, currency: 'SEK', is_business: null, journal_entry_id: null }],
|
|
[anna, twin],
|
|
)
|
|
expect(m.size).toBe(0)
|
|
})
|
|
})
|