fix(bookkeeping): gnubok_categorize_transaction now handles incoming expense refunds correctly (#761)

Incoming expense refunds (positive amount on an expense category) previously
booked with inverted debit/credit — crediting the bank and debiting the expense
account — which both imbalanced the book and reported negative bank flow.

getCategoryAccountMapping now detects amount > 0 on expense categories and
returns the reversed mapping: debit 1930, credit expense account, with 2641
as vatCreditAccount so ingående moms is correctly reversed on the VAT line.
The VAT line description is "Återföring ingående moms X%" rather than the
income-side "Utgående moms" label.

Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Flodén
2026-06-25 12:40:01 +02:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent bbc8063f53
commit 5279dfb494
2 changed files with 76 additions and 2 deletions
@@ -381,6 +381,64 @@ describe('private transaction accounts by entity type and direction', () => {
})
})
describe('incoming expense refund (positive amount, expense category)', () => {
it('getCategoryAccountMapping swaps accounts: bank debited, expense account credited', () => {
const result = getCategoryAccountMapping('expense_software', 500, true)
expect(result.debitAccount).toBe('1930')
expect(result.creditAccount).toBe('5420')
})
it('getCategoryAccountMapping sets vatCreditAccount 2641 and clears vatDebitAccount for refund', () => {
const result = getCategoryAccountMapping('expense_software', 500, true)
expect(result.vatDebitAccount).toBeNull()
expect(result.vatCreditAccount).toBe('2641')
})
it('VAT-exempt expense refund (bank_fees) has no VAT accounts', () => {
const result = getCategoryAccountMapping('expense_bank_fees', 100, true)
expect(result.debitAccount).toBe('1930')
expect(result.creditAccount).toBe('6570')
expect(result.vatDebitAccount).toBeNull()
expect(result.vatCreditAccount).toBeNull()
})
it('buildMappingResultFromCategory generates credit line on 2641 for expense refund', () => {
const tx = makeTransaction({ amount: 1000 })
const result = buildMappingResultFromCategory('expense_software', tx, true)
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2641')
expect(result.vat_lines[0].credit_amount).toBe(200)
expect(result.vat_lines[0].debit_amount).toBe(0)
})
it('buildMappingResultFromCategory uses återföring description for expense refund VAT', () => {
const tx = makeTransaction({ amount: 1000 })
const result = buildMappingResultFromCategory('expense_software', tx, true)
expect(result.vat_lines[0].description).toBe('Återföring ingående moms 25%')
})
it('buildMappingResultFromCategory generates no VAT line for VAT-exempt expense refund', () => {
const tx = makeTransaction({ amount: 100 })
const result = buildMappingResultFromCategory('expense_bank_fees', tx, true)
expect(result.vat_lines).toHaveLength(0)
})
it('buildMappingResultFromCategory maps debit/credit correctly (bank debited, expense credited)', () => {
const tx = makeTransaction({ amount: 1250 })
const result = buildMappingResultFromCategory('expense_software', tx, true)
expect(result.debit_account).toBe('1930')
expect(result.credit_account).toBe('5420')
})
it('vat_amount override on expense refund uses återföring description', () => {
const tx = makeTransaction({ amount: 1250 })
const result = buildMappingResultFromCategory('expense_software', tx, true, 'enskild_firma', 'standard_25', 200)
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].credit_amount).toBe(200)
expect(result.vat_lines[0].description).toBe('Återföring ingående moms (enligt underlag)')
})
})
describe('category default → leaf account guarantee', () => {
// BAS encodes the parent/leaf distinction in account_name via the
// "(gruppkonto)" suffix. Auditors and Skatteverket downstream reporting
+18 -2
View File
@@ -141,6 +141,18 @@ export function getCategoryAccountMapping(
// Note: income tax deduction was abolished 2017 (IL 16 kap 2 §), but VAT deduction remains.
const resolvedVat = vatTreatment ?? (isVatExempt ? null : category === 'expense_representation' ? 'reduced_12' : 'standard_25')
if (amount > 0) {
// Incoming refund: bank receives money, expense account is reduced (credited).
// Ingående moms is reversed — credit 2641 instead of debit.
return {
debitAccount: BANK_ACCOUNT,
creditAccount: expenseAccount,
vatTreatment: resolvedVat,
vatDebitAccount: null,
vatCreditAccount: resolvedVat ? '2641' : null,
}
}
return {
debitAccount: expenseAccount,
creditAccount: BANK_ACCOUNT,
@@ -291,12 +303,16 @@ export function buildMappingResultFromCategory(
description: hasVatOverride ? 'Ingående moms (enligt underlag)' : `Ingående moms ${vatRate * 100}%`,
})
} else if (vatAmount > 0 && transaction.amount > 0 && mapping.vatCreditAccount) {
// Income: Utgående moms (output VAT)
// Income output VAT, or expense refund reversing ingående moms (both credit vatCreditAccount).
// Distinguish by account: 2641 = reversed ingående moms, 2611/2621/2631 = utgående moms.
const isExpenseRefund = mapping.vatCreditAccount === '2641'
vatLines.push({
account_number: mapping.vatCreditAccount,
debit_amount: 0,
credit_amount: vatAmount,
description: hasVatOverride ? 'Utgående moms (enligt underlag)' : `Utgående moms ${vatRate * 100}%`,
description: isExpenseRefund
? (hasVatOverride ? 'Återföring ingående moms (enligt underlag)' : `Återföring ingående moms ${vatRate * 100}%`)
: (hasVatOverride ? 'Utgående moms (enligt underlag)' : `Utgående moms ${vatRate * 100}%`),
})
}
}