feat: unified document inbox, full BAS 2026, and document-transaction matching
- Expand BAS reference from ~180 to ~1,276 accounts (full BAS Kontoplan 2026) with K2 exclusion flags, per-class data files, and computed SRU codes - Evolve invoice inbox into unified document inbox handling invoices, receipts, and government letters with AI-powered classification (Claude Haiku Vision) - Add multi-pass document-to-transaction matching engine with greedy assignment for both supplier invoices (reference/amount/date/name) and receipts (weighted amount/merchant/date scoring) - Add supplier invoice matching in transaction ingest pipeline - Inject booking template suggestions into AI extraction prompts - Surface matched documents in swipe categorization UI with one-tap booking - Auto-activate missing BAS accounts during SIE import against full reference - Add K2 filter toggle in Chart of Accounts manager - Add receipt confirmation route with BFNAR representation fields - Add database migrations for K2 support and document matching columns - Remove obsolete extension migration scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6956a757f3
commit
39e407644d
@@ -4,6 +4,8 @@ import { NextResponse } from 'next/server'
|
||||
import { parseSIEFile, detectEncoding, decodeBuffer } from '@/lib/import/sie-parser'
|
||||
import { suggestMappings } from '@/lib/import/account-mapper'
|
||||
import { executeSIEImport } from '@/lib/import/sie-import'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
import type { AccountMapping, SIEAccountMappingRecord } from '@/lib/import/types'
|
||||
|
||||
/**
|
||||
@@ -54,24 +56,7 @@ export async function POST(request: Request) {
|
||||
if (mappingsJson) {
|
||||
mappings = JSON.parse(mappingsJson)
|
||||
} else {
|
||||
// Fetch user's full chart of accounts (paginated to avoid 1000-row limit)
|
||||
const basAccounts = await fetchAllRows(({ from, to }) =>
|
||||
supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.eq('is_active', true)
|
||||
.order('account_number')
|
||||
.range(from, to)
|
||||
)
|
||||
|
||||
if (basAccounts.length === 0) {
|
||||
return NextResponse.json({
|
||||
error: 'No chart of accounts found. Please complete onboarding first.',
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Load stored mappings
|
||||
// Match against full BAS reference (not just user's active chart)
|
||||
const { data: storedMappings } = await supabase
|
||||
.from('sie_account_mappings')
|
||||
.select('*')
|
||||
@@ -79,7 +64,7 @@ export async function POST(request: Request) {
|
||||
|
||||
mappings = suggestMappings(
|
||||
parsed.accounts,
|
||||
basAccounts,
|
||||
BAS_REFERENCE,
|
||||
(storedMappings as SIEAccountMappingRecord[]) || undefined
|
||||
)
|
||||
}
|
||||
@@ -97,6 +82,94 @@ export async function POST(request: Request) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Auto-activate any mapped BAS accounts not yet in the user's chart
|
||||
const mappedAccountNumbers = [
|
||||
...new Set(mappings.filter((m) => m.targetAccount).map((m) => m.targetAccount)),
|
||||
]
|
||||
|
||||
const existingAccounts = await fetchAllRows(({ from, to }) =>
|
||||
supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number')
|
||||
.eq('user_id', user.id)
|
||||
.in('account_number', mappedAccountNumbers)
|
||||
.range(from, to)
|
||||
)
|
||||
|
||||
// Build a lookup from SIE mappings for account names (used for bas_range accounts)
|
||||
const mappingNameLookup = new Map<string, string>()
|
||||
for (const m of mappings) {
|
||||
if (m.targetAccount) {
|
||||
mappingNameLookup.set(m.targetAccount, m.targetName || m.sourceName)
|
||||
}
|
||||
}
|
||||
|
||||
const existingNumbers = new Set(existingAccounts.map((a) => a.account_number))
|
||||
const accountsToActivate = mappedAccountNumbers
|
||||
.filter((num) => !existingNumbers.has(num))
|
||||
.map((num) => {
|
||||
const ref = getBASReference(num)
|
||||
if (ref) {
|
||||
// Account exists in BAS reference — use full metadata
|
||||
return {
|
||||
user_id: user.id,
|
||||
account_number: ref.account_number,
|
||||
account_name: ref.account_name,
|
||||
account_class: ref.account_class,
|
||||
account_group: ref.account_group,
|
||||
account_type: ref.account_type,
|
||||
normal_balance: ref.normal_balance,
|
||||
plan_type: 'full_bas' as const,
|
||||
is_active: true,
|
||||
is_system_account: false,
|
||||
description: ref.description,
|
||||
sru_code: ref.sru_code,
|
||||
sort_order: parseInt(ref.account_number),
|
||||
}
|
||||
}
|
||||
|
||||
// Account not in BAS reference (sub-account like 1241 Personbilar).
|
||||
// Derive metadata from the account number.
|
||||
const accountClass = parseInt(num.charAt(0), 10)
|
||||
const accountGroup = num.substring(0, 2)
|
||||
const accountName = mappingNameLookup.get(num) || `Konto ${num}`
|
||||
const accountType =
|
||||
accountClass === 1 ? 'asset'
|
||||
: accountClass === 2 ? 'liability'
|
||||
: accountClass === 3 ? 'revenue'
|
||||
: 'expense'
|
||||
const normalBalance =
|
||||
accountClass <= 1 || accountClass >= 4 ? 'debit' : 'credit'
|
||||
|
||||
return {
|
||||
user_id: user.id,
|
||||
account_number: num,
|
||||
account_name: accountName,
|
||||
account_class: accountClass,
|
||||
account_group: accountGroup,
|
||||
account_type: accountType,
|
||||
normal_balance: normalBalance,
|
||||
plan_type: 'full_bas' as const,
|
||||
is_active: true,
|
||||
is_system_account: false,
|
||||
description: accountName,
|
||||
sru_code: null,
|
||||
sort_order: parseInt(num),
|
||||
}
|
||||
})
|
||||
|
||||
if (accountsToActivate.length > 0) {
|
||||
const { error: activateError } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.insert(accountsToActivate)
|
||||
|
||||
if (activateError) {
|
||||
return NextResponse.json({
|
||||
error: `Failed to activate accounts: ${activateError.message}`,
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the import
|
||||
const result = await executeSIEImport(
|
||||
user.id,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
parseSIEFile,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
} from '@/lib/import/sie-parser'
|
||||
import { suggestMappings, getMappingStats } from '@/lib/import/account-mapper'
|
||||
import { generateImportPreview, checkDuplicateImport } from '@/lib/import/sie-import'
|
||||
import type { SIEAccountMappingRecord, SIEAccount } from '@/lib/import/types'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
|
||||
import type { SIEAccountMappingRecord } from '@/lib/import/types'
|
||||
|
||||
/**
|
||||
* POST /api/import/sie/parse
|
||||
@@ -78,33 +78,18 @@ export async function POST(request: Request) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Fetch user's full chart of accounts (paginated to avoid 1000-row limit)
|
||||
const basAccounts = await fetchAllRows(({ from, to }) =>
|
||||
supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.eq('is_active', true)
|
||||
.order('account_number')
|
||||
.range(from, to)
|
||||
)
|
||||
|
||||
if (basAccounts.length === 0) {
|
||||
return NextResponse.json({
|
||||
error: 'No chart of accounts found. Please complete onboarding first.',
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Fetch stored mappings from database
|
||||
const { data: storedMappings } = await supabase
|
||||
.from('sie_account_mappings')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
|
||||
// Suggest account mappings
|
||||
// Match against the full BAS reference (1,276 accounts) instead of only
|
||||
// the user's active chart (~40 accounts). Accounts that match will be
|
||||
// auto-activated during the execute step.
|
||||
const mappings = suggestMappings(
|
||||
parsed.accounts,
|
||||
basAccounts,
|
||||
BAS_REFERENCE,
|
||||
(storedMappings as SIEAccountMappingRecord[]) || undefined
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user