feat: enhance supplier invoices, entity-aware categorization, and new tests

Expand supplier invoice module with overdue cron job, credit note journal
entries, and event emissions on approve/mark-paid/create flows. Add entity
type (EF/AB) awareness to transaction categorization UI and category
mapping logic. Add comprehensive tests for supplier-invoice-entries,
transaction-entries, and expanded API route coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-03 17:08:26 +01:00
co-authored by Claude Opus 4.6
parent f6a9889f0a
commit a9bf24ce0b
23 changed files with 2293 additions and 113 deletions
@@ -96,11 +96,14 @@ export default function CategoryExpandedDialog({
key={cat.value}
variant="outline"
size="sm"
className="justify-start text-xs"
className="justify-start text-xs h-auto py-1.5"
onClick={() => handleSelectCategory(cat.value)}
disabled={isProcessing}
>
{cat.label}
<div className="flex flex-col items-start">
<span>{cat.label}</span>
{cat.account && <span className="text-[10px] text-muted-foreground">{cat.account}</span>}
</div>
</Button>
))}
</div>
@@ -113,11 +116,14 @@ export default function CategoryExpandedDialog({
key={cat.value}
variant="outline"
size="sm"
className="justify-start text-xs"
className="justify-start text-xs h-auto py-1.5"
onClick={() => handleSelectCategory(cat.value)}
disabled={isProcessing}
>
{cat.label}
<div className="flex flex-col items-start">
<span>{cat.label}</span>
{cat.account && <span className="text-[10px] text-muted-foreground">{cat.account}</span>}
</div>
</Button>
))}
</div>
@@ -5,7 +5,7 @@ import { formatCurrency } from '@/lib/utils'
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
import { getVatRate, extractVatAmount, extractNetAmount } from '@/lib/bookkeeping/vat-entries'
import { getCategoryAccountMapping } from '@/lib/bookkeeping/category-mapping'
import type { TransactionCategory, VatTreatment } from '@/types'
import type { TransactionCategory, VatTreatment, EntityType } from '@/types'
interface PreviewLine {
side: 'debet' | 'kredit'
@@ -19,6 +19,7 @@ interface JournalEntryPreviewProps {
category?: TransactionCategory
vatTreatment?: VatTreatment | 'none'
accountOverride?: string
entityType?: EntityType
/** For template-based bookings — overrides category mapping */
templateDebitAccount?: string
templateCreditAccount?: string
@@ -31,6 +32,7 @@ export default function JournalEntryPreview({
category,
vatTreatment,
accountOverride,
entityType = 'enskild_firma',
templateDebitAccount,
templateCreditAccount,
templateVatRate,
@@ -57,7 +59,7 @@ export default function JournalEntryPreview({
if (!category) return result
const resolvedVat = vatTreatment === 'none' ? undefined : vatTreatment
const mapping = getCategoryAccountMapping(category, amount, category !== 'private', 'enskild_firma', resolvedVat)
const mapping = getCategoryAccountMapping(category, amount, category !== 'private', entityType, resolvedVat)
const debitAccount = accountOverride && amount < 0 ? accountOverride : mapping.debitAccount
const creditAccount = accountOverride && amount > 0 ? accountOverride : mapping.creditAccount
@@ -91,7 +93,7 @@ export default function JournalEntryPreview({
}
return result
}, [amount, category, vatTreatment, accountOverride, templateDebitAccount, templateCreditAccount, templateVatRate])
}, [amount, category, vatTreatment, accountOverride, entityType, templateDebitAccount, templateCreditAccount, templateVatRate])
if (lines.length === 0) return null
@@ -15,7 +15,7 @@ import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
import VatTreatmentSelect from './VatTreatmentSelect'
import { VAT_TREATMENT_OPTIONS } from './transaction-types'
import type { TransactionWithInvoice } from './transaction-types'
import type { TransactionCategory, VatTreatment, BASAccount } from '@/types'
import type { TransactionCategory, VatTreatment, BASAccount, EntityType } from '@/types'
interface QuickReviewDialogProps {
open: boolean
@@ -25,6 +25,7 @@ interface QuickReviewDialogProps {
categoryLabel: string
defaultAccount: string
defaultVat: VatTreatment | 'none'
entityType?: EntityType
onConfirm: (
id: string,
category: TransactionCategory,
@@ -41,6 +42,7 @@ export default function QuickReviewDialog({
categoryLabel,
defaultAccount,
defaultVat,
entityType,
onConfirm,
}: QuickReviewDialogProps) {
const { toast } = useToast()
@@ -184,6 +186,7 @@ export default function QuickReviewDialog({
category={category}
vatTreatment={isLiabilityAccount ? 'none' : vatTreatment}
accountOverride={accountOverride}
entityType={entityType}
/>
{/* Account */}
@@ -220,7 +223,7 @@ export default function QuickReviewDialog({
className="text-xs text-primary hover:underline"
onClick={() => setShowVatDropdown(true)}
>
Andra
Ändra
</button>
</p>
)}
@@ -18,7 +18,7 @@ import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
import { X, ArrowLeft, ArrowRight, Building, AlertTriangle, Check, FileText, Link2, Receipt as ReceiptIcon, SkipForward, Paperclip, ChevronDown, ChevronUp, MessageSquareText } from 'lucide-react'
import DescribeTransactionDialog from './DescribeTransactionDialog'
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
import type { TransactionCategory, VatTreatment, BASAccount } from '@/types'
import type { TransactionCategory, VatTreatment, BASAccount, EntityType } from '@/types'
import type { SuggestedCategory, SuggestedTemplate } from '@/lib/transactions/category-suggestions'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import type { TransactionWithInvoice, CategorizeHandler, MatchInvoiceHandler } from './transaction-types'
@@ -31,6 +31,7 @@ interface SwipeCategorizationViewProps {
onCategorize: CategorizeHandler
onMatchInvoice?: MatchInvoiceHandler
onClose: () => void
entityType?: EntityType
}
const expenseCategories = EXPENSE_CATEGORIES
@@ -43,6 +44,7 @@ export default function SwipeCategorizationView({
onCategorize,
onMatchInvoice,
onClose,
entityType,
}: SwipeCategorizationViewProps) {
const { toast } = useToast()
const [showAllCategories, setShowAllCategories] = useState(false)
@@ -337,7 +339,10 @@ export default function SwipeCategorizationView({
onClick={() => handleCategorySelect(cat.value)}
disabled={isProcessing}
>
{cat.label}
<div className="flex items-baseline gap-2">
<span>{cat.label}</span>
{cat.account && <span className="text-xs text-muted-foreground">{cat.account}</span>}
</div>
</Button>
))}
</div>
@@ -408,6 +413,7 @@ export default function SwipeCategorizationView({
category={pendingCategory}
vatTreatment={isLiabilityAccount ? 'none' : vatTreatment}
accountOverride={accountOverride}
entityType={entityType}
/>
{/* Account override */}
@@ -444,7 +450,7 @@ export default function SwipeCategorizationView({
className="text-xs text-primary hover:underline"
onClick={() => setShowVatDropdown(true)}
>
Andra
Ändra
</button>
</p>
)}
+19 -18
View File
@@ -32,31 +32,32 @@ export type MatchInvoiceHandler = (
export interface CategoryOption {
value: TransactionCategory
label: string
account?: string
}
// Shared category arrays
export const EXPENSE_CATEGORIES: CategoryOption[] = [
{ value: 'expense_representation', label: 'Representation' },
{ value: 'expense_equipment', label: 'Utrustning' },
{ value: 'expense_software', label: 'Programvara' },
{ value: 'expense_consumables', label: 'Material' },
{ value: 'expense_travel', label: 'Resor' },
{ value: 'expense_office', label: 'Kontor' },
{ value: 'expense_vehicle', label: 'Bil & drivmedel' },
{ value: 'expense_telecom', label: 'Telefon & internet' },
{ value: 'expense_marketing', label: 'Marknadsföring' },
{ value: 'expense_professional_services', label: 'Konsulter' },
{ value: 'expense_education', label: 'Utbildning' },
{ value: 'expense_bank_fees', label: 'Bankavgift' },
{ value: 'expense_card_fees', label: 'Kortavgift' },
{ value: 'expense_currency_exchange', label: 'Valutaväxling' },
{ value: 'expense_other', label: 'Övrigt' },
{ value: 'expense_representation', label: 'Representation', account: '6071' },
{ value: 'expense_equipment', label: 'Utrustning', account: '5410' },
{ value: 'expense_software', label: 'Programvara', account: '5420' },
{ value: 'expense_consumables', label: 'Material', account: '5460' },
{ value: 'expense_travel', label: 'Resor', account: '5800' },
{ value: 'expense_office', label: 'Kontor', account: '6110' },
{ value: 'expense_vehicle', label: 'Bil & drivmedel', account: '5611' },
{ value: 'expense_telecom', label: 'Telefon & internet', account: '6200' },
{ value: 'expense_marketing', label: 'Marknadsföring', account: '5910' },
{ value: 'expense_professional_services', label: 'Konsulter', account: '6530' },
{ value: 'expense_education', label: 'Utbildning', account: '6991' },
{ value: 'expense_bank_fees', label: 'Bankavgift', account: '6570' },
{ value: 'expense_card_fees', label: 'Kortavgift', account: '6570' },
{ value: 'expense_currency_exchange', label: 'Valutaväxling', account: '7960' },
{ value: 'expense_other', label: 'Övrigt', account: '6991' },
]
export const INCOME_CATEGORIES: CategoryOption[] = [
{ value: 'income_services', label: 'Tjänster' },
{ value: 'income_products', label: 'Produkter' },
{ value: 'income_other', label: 'Övrigt' },
{ value: 'income_services', label: 'Tjänster', account: '3001' },
{ value: 'income_products', label: 'Produkter', account: '3001' },
{ value: 'income_other', label: 'Övrigt', account: '3900' },
]
export interface VatTreatmentOption {