- 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>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
1a725c121a
commit
3b9daf2606
@@ -18,6 +18,15 @@ describe('groupExpenseClaimsByPerson', () => {
|
||||
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' },
|
||||
|
||||
@@ -22,6 +22,16 @@ export interface ExpenseClaimRowForGrouping {
|
||||
expense_date: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The person behind a claim: the employee id, or for the owner the trimmed,
|
||||
* lower-cased name. Same rule as create_expense_payout_batch's claimant check
|
||||
* (lower(btrim(claimant_name))), so "Jakob" and "jakob " are one person here
|
||||
* exactly when the RPC would accept their claims in one payout.
|
||||
*/
|
||||
export function expenseClaimantKey(row: { employee_id: string | null; claimant_name: string }): string {
|
||||
return row.employee_id ?? `owner:${row.claimant_name.trim().toLowerCase()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Group registered claims into one item per person, oldest debt first.
|
||||
*
|
||||
@@ -34,7 +44,7 @@ export function groupExpenseClaimsByPerson(rows: ExpenseClaimRowForGrouping[]):
|
||||
const byPerson = new Map<string, ExpensePayoutDue>()
|
||||
for (const row of rows) {
|
||||
if (row.liability_account === '2018') continue
|
||||
const key = row.employee_id ?? `owner:${row.claimant_name}`
|
||||
const key = expenseClaimantKey(row)
|
||||
const amount = Number(row.amount_sek) || 0
|
||||
const existing = byPerson.get(key)
|
||||
if (existing) {
|
||||
|
||||
@@ -568,7 +568,7 @@ describe('listExpensePayoutsDue', () => {
|
||||
oldest_expense_date: '2026-09-02',
|
||||
},
|
||||
{
|
||||
key: 'owner:Jakob',
|
||||
key: 'owner:jakob',
|
||||
employee_id: null,
|
||||
claimant_name: 'Jakob',
|
||||
liability_account: '2893',
|
||||
|
||||
@@ -126,7 +126,7 @@ export type WorklistCategory = (typeof WORKLIST_CATEGORIES)[number]
|
||||
* (or claimant_name for the owner, who has no employee row).
|
||||
*/
|
||||
export interface ExpensePayoutDue {
|
||||
/** employee_id, or `owner:<claimant_name>` for claims without one. */
|
||||
/** employee_id, or `owner:<claimant_name trimmed and lower-cased>` for claims without one. */
|
||||
key: string
|
||||
employee_id: string | null
|
||||
claimant_name: string
|
||||
|
||||
Reference in New Issue
Block a user