feat: enable PSD2 bank integration via settings

- Uncomment Enable Banking extension in loader (now registered at runtime)
- Add subscriptionNotice field to ExtensionDefinition type
- Show confirmation dialog when enabling extensions with subscription requirements
- Fix Settings banking tab: toggle-aware visibility, URL-addressable tabs,
  BankSelector widget, correct API paths (/api/extensions/ext/enable-banking/*)
- Replace inline bank connection cards with BankConnectionStatus component
- Add actionable link to Settings from EnableBankingWorkspace
- Update CLAUDE.md with latest architecture docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-24 17:01:31 +01:00
co-authored by Claude Opus 4.6
parent 4163b46eae
commit 9d684d7e9f
10 changed files with 234 additions and 144 deletions
+2 -6
View File
@@ -14,11 +14,7 @@ import { intrastatExtension } from '@/extensions/export/intrastat'
import { currencyReceivablesExtension } from '@/extensions/export/currency-receivables'
import type { Extension } from './types'
// ── Enable Banking (PSD2) — opt-in extension ───────────────────────────
// Uncomment the following line to enable automatic PSD2 bank transaction sync.
// Requires ENABLE_BANKING_APP_ID and ENABLE_BANKING_PRIVATE_KEY env vars.
//
// import { enableBankingExtension } from '@/extensions/general/enable-banking'
import { enableBankingExtension } from '@/extensions/general/enable-banking'
/**
* Explicit list of first-party extensions.
@@ -36,7 +32,7 @@ const FIRST_PARTY_EXTENSIONS: Extension[] = [
invoiceInboxExtension,
calendarExtension,
userDescriptionMatchExtension,
// enableBankingExtension, // Uncomment to activate PSD2 bank sync
enableBankingExtension,
// ── Export sector ──────────────────────────────────────────
euSalesListExtension,
+2
View File
@@ -117,6 +117,8 @@ export const SECTORS: Sector[] = [
description: 'Automatisk banktransaktionssynk via PSD2',
longDescription:
'Koppla ditt bankkonto direkt och synka transaktioner automatiskt via säker PSD2-bankintegration. Stöder de flesta svenska banker.',
subscriptionNotice:
'Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera.',
},
],
},
+2
View File
@@ -39,6 +39,8 @@ export interface ExtensionDefinition {
readsCoreTables?: string[]
hasOwnData?: boolean
quickAction?: QuickActionDefinition
/** Notice shown when enabling — e.g. external subscription requirement */
subscriptionNotice?: string
}
/** Sector definition with its extensions */
+19 -10
View File
@@ -166,8 +166,8 @@ function accountToCategory(account: string, amount: number): string | null {
}
/**
* Source priority for sorting — higher-quality sources rank first.
* AI and mapping rules always beat history-based guesses.
* Source priority for tiebreaking — used when two suggestions
* have the same confidence score.
*/
const SOURCE_PRIORITY: Record<SuggestedCategory['source'], number> = {
mapping_rule: 3,
@@ -187,19 +187,28 @@ export function mergeAiSuggestions(
aiSuggestions: { category: string; basAccount: string; confidence: number; reasoning: string }[],
transactionAmount?: number
): SuggestedCategory[] {
const seen = new Set<string>(existing.map((s) => s.category))
const merged = [...existing]
for (const ai of aiSuggestions) {
if (seen.has(ai.category)) continue
// 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
}
seen.add(ai.category)
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,
@@ -210,12 +219,12 @@ export function mergeAiSuggestions(
})
}
// Sort by source priority first, then by confidence within the same tier
// Sort by confidence first, then by source priority as tiebreaker
return merged
.sort((a, b) => {
const priorityDiff = SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]
if (priorityDiff !== 0) return priorityDiff
return b.confidence - a.confidence
const confidenceDiff = b.confidence - a.confidence
if (confidenceDiff !== 0) return confidenceDiff
return SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]
})
.slice(0, 5)
}