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:
Jakob Wennberg
2026-02-25 16:59:02 +01:00
co-authored by Claude Opus 4.6
parent 6956a757f3
commit 39e407644d
68 changed files with 19234 additions and 2412 deletions
+36 -1
View File
@@ -15,7 +15,7 @@
DO $$
DECLARE
-- >>> SET THE TARGET USER EMAIL HERE <<<
target_email TEXT := 'user@example.com';
target_email TEXT := 'jakob.wennberg@arcim.io';
target_user_id UUID;
BEGIN
-- Resolve email to user ID
@@ -45,6 +45,17 @@ BEGIN
ALTER TABLE public.audit_log DISABLE TRIGGER USER;
ALTER TABLE public.company_settings DISABLE TRIGGER USER;
ALTER TABLE public.profiles DISABLE TRIGGER USER;
ALTER TABLE public.chat_sessions DISABLE TRIGGER USER;
ALTER TABLE public.invoice_inbox_items DISABLE TRIGGER USER;
-- Temporarily drop fiscal period constraints (migrations 042-043).
-- The NOT VALID CHECK constraints are still enforced on UPDATE, so
-- the circular-FK-breaking UPDATEs below will fail on rows with
-- non-conforming dates. The EXCLUDE constraint can also interfere.
-- We re-add them all after deletion.
ALTER TABLE public.fiscal_periods DROP CONSTRAINT IF EXISTS no_overlapping_fiscal_periods;
ALTER TABLE public.fiscal_periods DROP CONSTRAINT IF EXISTS fiscal_period_start_first_of_month;
ALTER TABLE public.fiscal_periods DROP CONSTRAINT IF EXISTS fiscal_period_end_last_of_month;
-- Break circular / self-referencing FK constraints
UPDATE public.fiscal_periods SET closing_entry_id = NULL, opening_balance_entry_id = NULL, previous_period_id = NULL WHERE user_id = target_user_id;
@@ -61,6 +72,7 @@ BEGIN
DELETE FROM public.invoice_reminders WHERE user_id = target_user_id;
DELETE FROM public.supplier_invoice_items WHERE supplier_invoice_id IN (SELECT id FROM public.supplier_invoices WHERE user_id = target_user_id);
DELETE FROM public.supplier_invoice_payments WHERE supplier_invoice_id IN (SELECT id FROM public.supplier_invoices WHERE user_id = target_user_id);
DELETE FROM public.invoice_inbox_items WHERE user_id = target_user_id;
DELETE FROM public.document_attachments WHERE user_id = target_user_id;
DELETE FROM public.journal_entry_lines WHERE journal_entry_id IN (SELECT id FROM public.journal_entries WHERE user_id = target_user_id);
DELETE FROM public.journal_entries WHERE user_id = target_user_id;
@@ -85,6 +97,9 @@ BEGIN
DELETE FROM public.cost_centers WHERE user_id = target_user_id;
DELETE FROM public.projects WHERE user_id = target_user_id;
DELETE FROM public.bank_file_imports WHERE user_id = target_user_id;
DELETE FROM public.chat_messages WHERE user_id = target_user_id;
DELETE FROM public.chat_sessions WHERE user_id = target_user_id;
DELETE FROM public.extension_data WHERE user_id = target_user_id;
DELETE FROM public.audit_log WHERE user_id = target_user_id;
DELETE FROM public.extension_toggles WHERE user_id = target_user_id;
DELETE FROM public.company_settings WHERE user_id = target_user_id;
@@ -107,6 +122,26 @@ BEGIN
ALTER TABLE public.audit_log ENABLE TRIGGER USER;
ALTER TABLE public.company_settings ENABLE TRIGGER USER;
ALTER TABLE public.profiles ENABLE TRIGGER USER;
ALTER TABLE public.chat_sessions ENABLE TRIGGER USER;
ALTER TABLE public.invoice_inbox_items ENABLE TRIGGER USER;
-- Re-add fiscal period constraints
ALTER TABLE public.fiscal_periods
ADD CONSTRAINT no_overlapping_fiscal_periods
EXCLUDE USING gist (
user_id WITH =,
daterange(period_start, period_end, '[]') WITH &&
);
ALTER TABLE public.fiscal_periods
ADD CONSTRAINT fiscal_period_start_first_of_month
CHECK (EXTRACT(DAY FROM period_start) = 1)
NOT VALID;
ALTER TABLE public.fiscal_periods
ADD CONSTRAINT fiscal_period_end_last_of_month
CHECK (period_end = (date_trunc('month', period_end) + interval '1 month - 1 day')::date)
NOT VALID;
-- Delete the auth user
DELETE FROM auth.users WHERE id = target_user_id;
-36
View File
@@ -1,36 +0,0 @@
import { cpSync, rmSync, existsSync } from 'fs'
import { join } from 'path'
const root = process.cwd()
const extensions = ['receipt-ocr', 'ai-categorization', 'ai-chat', 'push-notifications', 'enable-banking', 'example-logger']
for (const ext of extensions) {
const src = join(root, 'extensions', ext)
const dest = join(root, 'extensions', 'general', ext)
if (!existsSync(src)) {
console.log(`SKIP: ${src} does not exist`)
continue
}
if (existsSync(dest)) {
console.log(`CLEAN: ${dest} already exists, removing`)
rmSync(dest, { recursive: true, force: true })
}
console.log(`COPY: ${src} -> ${dest}`)
cpSync(src, dest, { recursive: true })
}
console.log('Done copying extensions to general/')
// Now remove the old directories
for (const ext of extensions) {
const src = join(root, 'extensions', ext)
if (existsSync(src)) {
console.log(`REMOVE: ${src}`)
rmSync(src, { recursive: true, force: true })
}
}
console.log('Done removing old extension directories')
-33
View File
@@ -1,33 +0,0 @@
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const extsDir = path.join(root, 'extensions');
const generalDir = path.join(extsDir, 'general');
const dirs = [
'receipt-ocr',
'ai-categorization',
'ai-chat',
'push-notifications',
'enable-banking',
'example-logger',
];
if (!fs.existsSync(generalDir)) {
fs.mkdirSync(generalDir, { recursive: true });
}
for (const ext of dirs) {
const src = path.join(extsDir, ext);
const dest = path.join(generalDir, ext);
if (fs.existsSync(src)) {
fs.cpSync(src, dest, { recursive: true });
fs.rmSync(src, { recursive: true, force: true });
console.log('Done: ' + ext);
} else {
console.log('Skip: ' + ext);
}
}
console.log('All done.');
-1
View File
@@ -1 +0,0 @@
const fs=require('fs'),p=require('path'),r=p.resolve(__dirname,'..'),b=p.join(r,'extensions'),t=p.join(b,'general'),d=['receipt-ocr','ai-categorization','ai-chat','push-notifications','enable-banking','example-logger'];fs.mkdirSync(t,{recursive:!0});d.forEach(n=>{const s=p.join(b,n),e=p.join(t,n);fs.existsSync(s)?(fs.cpSync(s,e,{recursive:!0}),fs.rmSync(s,{recursive:!0,force:!0}),console.log(n)):console.log('!'+n)});