Invoice fix and new inbound invoice extension etc

This commit is contained in:
Jakob Wennberg
2026-02-21 17:41:14 +01:00
parent 43172751e9
commit 59d935f2cc
41 changed files with 4698 additions and 720 deletions
@@ -1,5 +1,10 @@
import { describe, it, expect } from 'vitest'
import { getCategoryAccountMapping, getExpenseAccountForCategory } from '../category-mapping'
import {
getCategoryAccountMapping,
getExpenseAccountForCategory,
getDefaultAccountForCategory,
getDefaultVatTreatmentForCategory,
} from '../category-mapping'
describe('getCategoryAccountMapping', () => {
describe('income_products uses correct account', () => {
@@ -43,3 +48,62 @@ describe('getExpenseAccountForCategory', () => {
expect(getExpenseAccountForCategory('expense_bank_fees')).toBe('6570')
})
})
describe('getDefaultAccountForCategory', () => {
it('returns expense account for expense categories', () => {
expect(getDefaultAccountForCategory('expense_equipment')).toBe('5410')
expect(getDefaultAccountForCategory('expense_software')).toBe('5420')
expect(getDefaultAccountForCategory('expense_travel')).toBe('5800')
expect(getDefaultAccountForCategory('expense_bank_fees')).toBe('6570')
})
it('returns income account for income categories', () => {
expect(getDefaultAccountForCategory('income_services')).toBe('3001')
expect(getDefaultAccountForCategory('income_products')).toBe('3001')
expect(getDefaultAccountForCategory('income_other')).toBe('3900')
})
it('returns private account for enskild firma', () => {
expect(getDefaultAccountForCategory('private', 'enskild_firma')).toBe('2013')
})
it('returns private account for aktiebolag', () => {
expect(getDefaultAccountForCategory('private', 'aktiebolag')).toBe('2893')
})
it('returns entity-specific education account', () => {
expect(getDefaultAccountForCategory('expense_education', 'enskild_firma')).toBe('6991')
expect(getDefaultAccountForCategory('expense_education', 'aktiebolag')).toBe('7610')
})
it('returns fallback for uncategorized', () => {
expect(getDefaultAccountForCategory('uncategorized')).toBe('6991')
})
})
describe('getDefaultVatTreatmentForCategory', () => {
it('returns standard_25 for regular expense categories', () => {
expect(getDefaultVatTreatmentForCategory('expense_equipment')).toBe('standard_25')
expect(getDefaultVatTreatmentForCategory('expense_software')).toBe('standard_25')
expect(getDefaultVatTreatmentForCategory('expense_travel')).toBe('standard_25')
})
it('returns standard_25 for income categories', () => {
expect(getDefaultVatTreatmentForCategory('income_services')).toBe('standard_25')
expect(getDefaultVatTreatmentForCategory('income_products')).toBe('standard_25')
})
it('returns null for VAT-exempt categories', () => {
expect(getDefaultVatTreatmentForCategory('expense_bank_fees')).toBeNull()
expect(getDefaultVatTreatmentForCategory('expense_card_fees')).toBeNull()
expect(getDefaultVatTreatmentForCategory('expense_currency_exchange')).toBeNull()
})
it('returns null for private transactions', () => {
expect(getDefaultVatTreatmentForCategory('private')).toBeNull()
})
it('returns null for uncategorized', () => {
expect(getDefaultVatTreatmentForCategory('uncategorized')).toBeNull()
})
})
+66
View File
@@ -257,3 +257,69 @@ export function getExpenseAccountForCategory(category: TransactionCategory): str
}
return mapping[category] || null
}
/**
* Get the default account number for a category.
* For expense categories: returns the expense account (debit side).
* For income categories: returns the revenue account (credit side).
* For private/uncategorized: returns the entity-specific private or fallback account.
*/
export function getDefaultAccountForCategory(
category: TransactionCategory,
entityType: EntityType = 'enskild_firma'
): string {
if (category === 'private') {
return PRIVATE_ACCOUNTS[entityType] || PRIVATE_ACCOUNTS.enskild_firma
}
const expenseMapping: Record<string, string> = {
expense_equipment: '5410',
expense_software: '5420',
expense_travel: '5800',
expense_office: '5010',
expense_marketing: '5910',
expense_professional_services: '6530',
expense_education: entityType === 'aktiebolag' ? '7610' : '6991',
expense_bank_fees: '6570',
expense_card_fees: '6570',
expense_currency_exchange: '7960',
expense_other: '6991',
}
if (category.startsWith('expense_')) {
return expenseMapping[category] || '6991'
}
const incomeMapping: Record<string, string> = {
income_services: '3001',
income_products: '3001',
income_other: '3900',
}
if (category.startsWith('income_')) {
return incomeMapping[category] || '3900'
}
// uncategorized
return '6991'
}
/**
* Get the default VAT treatment for a category.
* Bank fees, card fees, and currency exchange are VAT-exempt.
* All other business categories default to standard 25%.
*/
export function getDefaultVatTreatmentForCategory(
category: TransactionCategory
): VatTreatment | null {
if (category === 'private' || category === 'uncategorized') {
return null
}
const vatExemptCategories = ['expense_bank_fees', 'expense_card_fees', 'expense_currency_exchange']
if (vatExemptCategories.includes(category)) {
return null
}
return 'standard_25'
}
+6
View File
@@ -11,6 +11,8 @@ import type {
CAMT054Notification,
AuditSecurityEvent,
ReconciliationMethod,
InvoiceInboxItem,
SupplierInvoice,
} from '@/types'
// ============================================================
@@ -62,6 +64,10 @@ export type CoreEvent =
privateTotal: number;
userId: string;
}}
// Supplier Invoice Inbox
| { type: 'supplier_invoice.received'; payload: { inboxItem: InvoiceInboxItem; userId: string } }
| { type: 'supplier_invoice.extracted'; payload: { inboxItem: InvoiceInboxItem; confidence: number; userId: string } }
| { type: 'supplier_invoice.confirmed'; payload: { inboxItem: InvoiceInboxItem; supplierInvoice: SupplierInvoice; userId: string } }
// Audit
| { type: 'audit.security_event'; payload: { event: AuditSecurityEvent; userId: string } }
+2 -2
View File
@@ -11,8 +11,8 @@ describe('sectors registry', () => {
expect(SECTORS.length).toBe(6)
})
it('should have 17 total extensions', () => {
expect(getAllExtensions().length).toBe(17)
it('should have 18 total extensions', () => {
expect(getAllExtensions().length).toBe(18)
})
it('should have unique slugs within each sector', () => {
+2
View File
@@ -5,6 +5,7 @@ import { pushNotificationsExtension } from '@/extensions/general/push-notificati
import { sruExportExtension } from '@/extensions/sru-export'
import { neBilagaExtension } from '@/extensions/ne-bilaga'
import { aiChatExtension } from '@/extensions/general/ai-chat'
import { invoiceInboxExtension } from '@/extensions/general/invoice-inbox'
import type { Extension } from './types'
// ── Enable Banking (PSD2) — opt-in extension ───────────────────────────
@@ -26,6 +27,7 @@ const FIRST_PARTY_EXTENSIONS: Extension[] = [
sruExportExtension,
neBilagaExtension,
aiChatExtension,
invoiceInboxExtension,
// enableBankingExtension, // Uncomment to activate PSD2 bank sync
]
+12
View File
@@ -64,6 +64,18 @@ export const SECTORS: Sector[] = [
longDescription:
'Få push-notiser direkt i webbläsaren när viktiga händelser sker — nya fakturor, förfallna betalningar, slutförda bokföringar med mera.',
},
{
slug: 'invoice-inbox',
name: 'Leverantörsfaktura-inbox',
sector: 'general',
category: 'import',
icon: 'Inbox',
dataPattern: 'manual',
hasOwnData: true,
description: 'Ta emot leverantörsfakturor via e-post eller uppladdning',
longDescription:
'Skicka leverantörsfakturor till en dedikerad e-postadress eller ladda upp manuellt. AI extraherar automatiskt leverantörsdata, belopp och moms. Granska och bekräfta med ett klick för att skapa leverantörsfakturor.',
},
{
slug: 'enable-banking',
name: 'Bankintegration (PSD2)',