feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157)

Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr,
invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/
Anthropic/OpenAI deps) to simplify core and reduce bundle size.

Restructure monolithic settings page into dedicated sub-pages (company,
bookkeeping, invoicing, tax, banking, api, account, team, templates) with
shared layout and sidebar navigation.

Add atomic commit_journal_entry RPC so voucher number increment and status
update happen in a single transaction — prevents burned numbers on constraint
failures. Add continuity check report and voucher gap explanation tracking.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-01 17:08:00 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent e89f2c402d
commit d0b3f21bde
172 changed files with 3621 additions and 20538 deletions
+1 -98
View File
@@ -1,7 +1,6 @@
import { suggestCategory } from '@/lib/tax/expense-warnings'
import { getExpenseAccountForCategory } from '@/lib/bookkeeping/category-mapping'
import { findMatchingTemplates, getTemplateById, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
import { extensionRegistry } from '@/lib/extensions/registry'
import type { Transaction, TransactionCategory, EntityType, MappingRule, LinePatternEntry } from '@/types'
export interface SuggestedCategory {
@@ -9,7 +8,7 @@ export interface SuggestedCategory {
label: string
account: string | null
confidence: number
source: 'mapping_rule' | 'pattern' | 'history' | 'ai'
source: 'mapping_rule' | 'pattern' | 'history'
match_reason?: string
}
@@ -165,70 +164,6 @@ function accountToCategory(account: string, amount: number): string | null {
return expenseMap[account] || null
}
/**
* Source priority for tiebreaking — used when two suggestions
* have the same confidence score.
*/
const SOURCE_PRIORITY: Record<SuggestedCategory['source'], number> = {
mapping_rule: 3,
ai: 2,
pattern: 1,
history: 0,
}
/**
* Merge AI-generated suggestions into existing suggestion list.
* AI suggestions take priority over history-based ones.
* Deduplicates by category, preserving the higher-confidence entry.
* When transactionAmount is provided, filters out wrong-direction suggestions.
*/
export function mergeAiSuggestions(
existing: SuggestedCategory[],
aiSuggestions: { category: string; basAccount: string; confidence: number; reasoning: string }[],
transactionAmount?: number
): SuggestedCategory[] {
const merged = [...existing]
for (const ai of aiSuggestions) {
// Skip suggestions that don't match transaction direction
if (transactionAmount !== undefined) {
if (transactionAmount > 0 && ai.category.startsWith('expense_')) continue
if (transactionAmount < 0 && ai.category.startsWith('income_')) continue
}
const existingIdx = merged.findIndex((s) => s.category === ai.category)
if (existingIdx !== -1) {
// Upgrade existing entry if the AI has higher confidence
if (ai.confidence > merged[existingIdx].confidence) {
merged[existingIdx] = {
...merged[existingIdx],
account: ai.basAccount || merged[existingIdx].account,
confidence: ai.confidence,
source: 'ai',
}
}
continue
}
merged.push({
category: ai.category as TransactionCategory,
label: CATEGORY_LABELS[ai.category] || ai.category,
account: ai.basAccount || null,
confidence: ai.confidence,
source: 'ai',
})
}
// Sort by confidence first, then by source priority as tiebreaker
return merged
.sort((a, b) => {
const confidenceDiff = b.confidence - a.confidence
if (confidenceDiff !== 0) return confidenceDiff
return SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]
})
.slice(0, 5)
}
// ============================================================
// Template Suggestions
// ============================================================
@@ -340,38 +275,6 @@ export async function getSuggestedTemplates(
}
}
// 3. If AI extension loaded, merge in embedding-based matches (higher confidence)
try {
const aiExt = extensionRegistry.get('ai-categorization')
if (aiExt?.services?.findSimilarTemplates) {
const aiMatches: TemplateMatch[] = await aiExt.services.findSimilarTemplates(transaction, entityType)
for (const m of aiMatches) {
const existing = results.find((r) => r.template_id === m.template.id)
if (existing) {
// AI match upgrades confidence if higher
if (m.confidence > existing.confidence) {
existing.confidence = m.confidence
}
} else {
results.push({
template_id: m.template.id,
name_sv: m.template.name_sv,
name_en: m.template.name_en,
group: m.template.group,
debit_account: m.template.debit_account,
credit_account: m.template.credit_account,
confidence: m.confidence,
description_sv: m.template.description_sv,
risk_level: m.template.risk_level,
requires_review: m.template.requires_review,
})
}
}
}
} catch {
// AI enhancement is non-blocking
}
return results
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 10)