feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup
- Update BAS account catalog with comprehensive SRU codes and K2 flags - Add currency revaluation service with tests and API route - Add expenses page and account deletion API - Enhance booking templates with new patterns and improved tests - Improve transaction categorization with template picker and description matching - Polish dashboard, onboarding, import, and transaction UIs - Refactor year-end service for multi-step closing - Move SRU generator to ne-bilaga, remove standalone SRU export - Remove unused dev docs, mock data, and extension hooks - Add invoice delivery note sequences migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a9bf24ce0b
commit
66a4027f1e
@@ -1,6 +1,6 @@
|
||||
import { suggestCategory } from '@/lib/tax/expense-warnings'
|
||||
import { getExpenseAccountForCategory } from '@/lib/bookkeeping/category-mapping'
|
||||
import { findMatchingTemplates, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
|
||||
import { findMatchingTemplates, getTemplateById, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
|
||||
import { extensionRegistry } from '@/lib/extensions/registry'
|
||||
import type { Transaction, TransactionCategory, EntityType, MappingRule } from '@/types'
|
||||
|
||||
@@ -246,42 +246,132 @@ export interface SuggestedTemplate {
|
||||
requires_review: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recently used templates from mapping rules.
|
||||
* Extracts unique template_id values and returns them as suggestions.
|
||||
*/
|
||||
export function getRecentlyUsedTemplates(
|
||||
mappingRules: MappingRule[],
|
||||
entityType?: EntityType,
|
||||
direction?: 'expense' | 'income' | 'transfer'
|
||||
): SuggestedTemplate[] {
|
||||
const seen = new Set<string>()
|
||||
const results: SuggestedTemplate[] = []
|
||||
|
||||
// Sort by most recent (highest priority first)
|
||||
const sorted = [...mappingRules]
|
||||
.filter((r) => r.is_active && r.template_id)
|
||||
.sort((a, b) => (b.confidence_score || 0) - (a.confidence_score || 0))
|
||||
|
||||
for (const rule of sorted) {
|
||||
if (!rule.template_id || seen.has(rule.template_id)) continue
|
||||
seen.add(rule.template_id)
|
||||
|
||||
const template = getTemplateById(rule.template_id)
|
||||
if (!template) continue
|
||||
|
||||
// Filter by entity applicability
|
||||
if (entityType && template.entity_applicability !== 'all' && template.entity_applicability !== entityType) continue
|
||||
|
||||
// Filter by direction
|
||||
if (direction && template.direction !== direction && template.direction !== 'transfer') continue
|
||||
|
||||
results.push({
|
||||
template_id: template.id,
|
||||
name_sv: template.name_sv,
|
||||
name_en: template.name_en,
|
||||
group: template.group,
|
||||
debit_account: template.debit_account,
|
||||
credit_account: template.credit_account,
|
||||
confidence: 0.85,
|
||||
description_sv: template.description_sv,
|
||||
risk_level: template.risk_level,
|
||||
requires_review: template.requires_review,
|
||||
})
|
||||
|
||||
if (results.length >= 5) break
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested booking templates for a transaction.
|
||||
* Tries embedding-based semantic search first, falls back to keyword matching.
|
||||
* Keyword matching as primary, AI embedding search as optional enhancer.
|
||||
*/
|
||||
export async function getSuggestedTemplates(
|
||||
transaction: Transaction,
|
||||
entityType?: EntityType
|
||||
entityType?: EntityType,
|
||||
mappingRules?: MappingRule[]
|
||||
): Promise<SuggestedTemplate[]> {
|
||||
let matches: TemplateMatch[]
|
||||
const seen = new Set<string>()
|
||||
const results: SuggestedTemplate[] = []
|
||||
|
||||
// 1. Boost recently-used templates from mapping rules
|
||||
if (mappingRules) {
|
||||
const direction = transaction.amount < 0 ? 'expense' : 'income'
|
||||
const recent = getRecentlyUsedTemplates(mappingRules, entityType, direction)
|
||||
for (const r of recent) {
|
||||
if (!seen.has(r.template_id)) {
|
||||
seen.add(r.template_id)
|
||||
results.push(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Keyword + MCC matching (always available, no API keys needed)
|
||||
const keywordMatches = findMatchingTemplates(transaction, entityType)
|
||||
for (const m of keywordMatches) {
|
||||
if (!seen.has(m.template.id)) {
|
||||
seen.add(m.template.id)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If AI extension loaded, merge in embedding-based matches (higher confidence)
|
||||
try {
|
||||
const aiExt = extensionRegistry.get('ai-categorization')
|
||||
if (aiExt?.services?.findSimilarTemplates) {
|
||||
matches = await aiExt.services.findSimilarTemplates(transaction, entityType)
|
||||
} else {
|
||||
matches = []
|
||||
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 {
|
||||
matches = []
|
||||
// AI enhancement is non-blocking
|
||||
}
|
||||
|
||||
// Fall back to keyword matching if embedding search returns nothing
|
||||
if (matches.length === 0) {
|
||||
matches = findMatchingTemplates(transaction, entityType)
|
||||
}
|
||||
|
||||
return matches.map((m: TemplateMatch) => ({
|
||||
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,
|
||||
}))
|
||||
return results
|
||||
.sort((a, b) => b.confidence - a.confidence)
|
||||
.slice(0, 10)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user