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
+60 -21
View File
@@ -34,38 +34,43 @@ app/
(onboarding)/ 6-step setup wizard
(dashboard)/ Authenticated routes (invoices, customers, transactions,
bookkeeping, reports, suppliers, supplier-invoices,
receipts, settings, calendar, help, import,
receipts, deadlines, settings, help, import,
extensions, e/[sector]/[slug])
(public)/ Public invoice action links (no auth)
api/ API routes organized by domain
components/
ui/ shadcn/ui primitives (button, card, dialog, table, etc.)
bookkeeping/ Chart of accounts manager, account combobox, add/edit dialogs
calendar/ Calendar views, deadline cards, payment summary, todo widgets
bookkeeping/ Chart of accounts manager, account combobox, add/edit dialogs,
journal entry form/list/review, document upload, correction dialog
chat/ ChatWidget, ChatPanel, ChatInput, ChatMessage
customers/ CustomerForm
dashboard/ DashboardContent, DashboardNav, FSkattWarningCard
deadlines/ DeadlineCard, DeadlineFilters, DeadlineForm, DeadlineList,
TaxTodoWidget, UpcomingDeadlinesWidget
extensions/ Extension marketplace UI (ExtensionCard, SectorCard,
ExtensionToggleButton, workspace components,
per-sector subdirectories)
import/ Bank file import workflow components
per-sector subdirectories, shared reusable components)
import/ Bank file import workflow components (SIE + bank file steps)
invoices/ InvoiceReviewContent
onboarding/ NewUserChecklist, setup step components
reports/ Report views (including BankReconciliationView)
reports/ Report views (BankReconciliationView, charts)
settings/ CalendarFeedSettings
suppliers/ Supplier-related components
transactions/ Transaction list, categorization, booking components
suppliers/ SupplierForm, SupplierInvoiceReviewContent
transactions/ Transaction list, categorization, booking, swipe review,
batch operations, invoice matching, VAT treatment
extensions/ Sector-based extension hierarchy
general/ General-purpose extensions (all businesses)
ai-categorization/ AI-powered transaction categorization
ai-chat/ Claude-based chat assistant (LangChain RAG)
calendar/ Payment calendar views, deadline cards, payment summary
enable-banking/ PSD2 bank integration (opt-in, commented out in loader)
example-logger/ Minimal reference extension (not loaded)
invoice-inbox/ Supplier invoice intake via email/upload with AI extraction
push-notifications/ Web push notification system
receipt-ocr/ Receipt image OCR processing (includes components/pages)
user-description-match/ User description matching for transaction categorization
restaurant/ Restaurant & cafe sector
food-cost/ Food cost percentage calculator
earnings-per-liter/ Revenue per liter of alcohol
@@ -100,6 +105,9 @@ lib/
vat-entries.ts VAT line generation
bas-reference.ts BAS account catalog (~180 accounts with metadata, SRU codes)
account-descriptions.ts Human-readable account name lookup
booking-templates.ts Booking template patterns for AI suggestions
template-embeddings.ts AI embeddings for booking template matching
client-account-names.ts Custom account name management
validate-period-duration.ts Fiscal period duration validation (BFL 3 kap.)
handlers/ Booking handler functions (supplier-invoice-handler.ts)
core/
@@ -111,6 +119,7 @@ lib/
currency/ Riksbanken exchange rates
deadlines/ Tax deadline tracking, status engine
email/ Email service (Resend), invoice/reminder templates
errors/ Error message utilities (get-error-message.ts)
events/ Event bus (bus.ts, types.ts)
extensions/ Extension system
loader.ts FIRST_PARTY_EXTENSIONS array, static imports
@@ -125,9 +134,13 @@ lib/
icon-resolver.tsx Dynamic icon lookup for extensions
use-account-totals.ts Hook for account balance queries
use-extension-data.ts Hook for extension-specific data
invoice-inbox-utils.ts Utilities for supplier invoice inbox
use-mock-data.ts Mock data utilities for development
hooks/ React hooks (use-unsaved-changes.ts)
import/ SIE parser, SIE import orchestrator, bank file parser
bank-file/ Bank file parser with format modules
formats/ camt053, generic-csv, handelsbanken, nordea, seb, swedbank
formats/ camt053, generic-csv, handelsbanken, nordea, seb, swedbank,
ica-banken, lansforsakringar, lunar, skandia
invoices/ VAT rules, invoice matching, PDF template, reminder processor
reconciliation/ Bank reconciliation engine (4-pass matching algorithm)
reports/ Financial reports (trial-balance, income-statement,
@@ -137,9 +150,12 @@ lib/
ar-ledger, ar-reconciliation, monthly-breakdown)
supabase/ Client setup (client.ts = browser, server.ts = server/admin,
fetch-all.ts = pagination helper, middleware.ts)
tax/ Tax calculations, deadlines, Swedish holidays, expense warnings
tax/ Tax calculations, deadlines, deadline generator,
Swedish holidays, expense warnings
transactions/ Transaction processing, category suggestions
vat/ VAT utilities (VIES client for EU VAT validation)
init.ts Extension loader (idempotent, called by API routes)
logger.ts Centralized logging utility
utils.ts Shared utility functions
types/index.ts Canonical type definitions (single source of truth)
@@ -270,14 +286,16 @@ Key types (from `lib/extensions/types.ts`):
### Currently Loaded Extensions (FIRST_PARTY_EXTENSIONS)
```
receiptOcrExtension @/extensions/general/receipt-ocr
aiCategorizationExtension @/extensions/general/ai-categorization
pushNotificationsExtension @/extensions/general/push-notifications
sruExportExtension @/extensions/sru-export
neBilagaExtension @/extensions/ne-bilaga
aiChatExtension @/extensions/general/ai-chat
invoiceInboxExtension @/extensions/general/invoice-inbox
# enableBankingExtension @/extensions/general/enable-banking (commented out, opt-in)
receiptOcrExtension @/extensions/general/receipt-ocr
aiCategorizationExtension @/extensions/general/ai-categorization
pushNotificationsExtension @/extensions/general/push-notifications
sruExportExtension @/extensions/sru-export
neBilagaExtension @/extensions/ne-bilaga
aiChatExtension @/extensions/general/ai-chat
invoiceInboxExtension @/extensions/general/invoice-inbox
calendarExtension @/extensions/general/calendar
userDescriptionMatchExtension @/extensions/general/user-description-match
# enableBankingExtension @/extensions/general/enable-banking (commented out, opt-in)
```
### Extension Interface
@@ -430,16 +448,35 @@ mockResult({ data: makeTransaction(), error: null })
- `lib/api/__tests__/validate.test.ts` — Body/query validation helpers
- `lib/bookkeeping/__tests__/engine.test.ts` — Balance validation
- `lib/bookkeeping/__tests__/invoice-entries.test.ts` — Per-line VAT, mixed-rate invoices, credit notes
- `lib/bookkeeping/__tests__/mapping-engine.test.ts` — Auto-categorization rule matching
- `lib/bookkeeping/__tests__/category-mapping.test.ts` — Category-to-account mapping
- `lib/bookkeeping/__tests__/booking-templates.test.ts` — Booking template patterns
- `lib/bookkeeping/__tests__/template-embeddings.test.ts` — AI embedding matching
- `lib/bookkeeping/__tests__/validate-period-duration.test.ts` — Fiscal period duration
- `lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts` — Supplier booking handler
- `lib/core/bookkeeping/__tests__/storno-service.test.ts` — Complex mock queues
- `lib/core/bookkeeping/__tests__/period-service.test.ts` — Fiscal period management
- `lib/core/bookkeeping/__tests__/year-end-service.test.ts` — Year-end closing
- `lib/core/documents/__tests__/document-service.test.ts` — Storage mocking
- `lib/core/tax/__tests__/tax-code-service.test.ts` — Tax code management
- `lib/currency/__tests__/riksbanken.test.ts` — Exchange rate fetching
- `lib/events/__tests__/bus.test.ts` — Event bus behavior
- `lib/extensions/__tests__/registry.test.ts` — Extension registration
- `lib/extensions/__tests__/loader.test.ts` — Extension loader
- `lib/extensions/__tests__/toggle-check.test.ts` — Extension toggle logic
- `lib/extensions/__tests__/validation.test.ts` — Extension data validation
- `lib/extensions/__tests__/sectors.test.ts` — Sector registry
- `lib/extensions/__tests__/context-factory.test.ts` — Extension context builder
- `lib/extensions/__tests__/invoice-inbox-utils.test.ts` — Invoice inbox utilities
- `lib/import/__tests__/sie-parser.test.ts` — SIE file parsing
- `lib/import/__tests__/sie-import.test.ts` — SIE import orchestration
- `lib/import/__tests__/account-mapper.test.ts` — SIE account mapping
- `lib/import/bank-file/__tests__/parser.test.ts` — Bank file format parsing
- `lib/reconciliation/__tests__/bank-reconciliation.test.ts` — Reconciliation matching algorithm
- `lib/reports/__tests__/vat-declaration.test.ts` — VAT declaration report
- `lib/tax/__tests__/deadline-config.test.ts` — Tax deadline configuration
- `lib/transactions/__tests__/ingest.test.ts` — Transaction ingestion and dedup
- `lib/vat/__tests__/vies-client.test.ts` — VIES VAT number validation
---
@@ -447,11 +484,11 @@ mockResult({ data: makeTransaction(), error: null })
### Location
`supabase/migrations/` — currently 39 files numbered `20240101000001` through `20240101000038` (note: two files share number `000033`).
`supabase/migrations/` — currently 41 files numbered `20240101000001` through `20240101000041`.
### Naming Convention
`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000039_*.sql`
`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000042_*.sql`
### Migration Rules
@@ -491,12 +528,14 @@ mockResult({ data: makeTransaction(), error: null })
- **Migration 031 (`invoice_document_type`)** — Adds `document_type` column to `invoices` (CHECK: invoice/proforma/delivery_note, default 'invoice') and `converted_from_id` FK for tracking proforma-to-invoice conversions.
- **Migration 032 (`add_accounting_method`)** — Adds `accounting_method` column to `company_settings` (CHECK: accrual/cash, default 'accrual') to support kontantmetoden vs faktureringsmetoden.
- **Migration 033 (`ai_chat_schema`)** — AI chat conversation and message storage.
- **Migration 033 (`invoice_inbox`)** — Invoice inbox table for supplier invoice intake. (Note: shares number with ai_chat_schema.)
- **Migration 034 (`fix_extension_data_trigger`)** — Fixes extension data trigger.
- **Migration 035 (`fix_push_notifications`)** — Push notifications schema fix.
- **Migration 036 (`fix_enable_banking`)** — Enable Banking schema fix.
- **Migration 037 (`extension_toggles`)** — Extension toggle table for per-user enable/disable.
- **Migration 038 (`fix_match_documents_search_path`)** — Fixes search path for document matching function.
- **Migration 039 (`invoice_inbox`)** — Invoice inbox table for supplier invoice intake via email/upload.
- **Migration 040 (`booking_template_embeddings`)** — Booking templates with AI embeddings for suggestion matching.
- **Migration 041 (`user_description_matching`)** — User description matching for transaction categorization.
---
@@ -56,7 +56,11 @@ export default async function ExtensionDetailPage({
</div>
</div>
</div>
<ExtensionToggleButton sectorSlug={sectorSlug} extensionSlug={extensionSlug} />
<ExtensionToggleButton
sectorSlug={sectorSlug}
extensionSlug={extensionSlug}
subscriptionNotice={definition.subscriptionNotice}
/>
</div>
{/* Details */}
+67 -89
View File
@@ -8,18 +8,13 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/components/ui/use-toast'
import { formatDate } from '@/lib/utils'
import {
Loader2,
Building,
CreditCard,
User,
AlertTriangle,
RefreshCw,
Trash2,
LogOut,
Bell,
Calendar,
@@ -27,11 +22,14 @@ import {
Moon,
Monitor,
Palette,
Landmark,
} from 'lucide-react'
import { useTheme } from 'next-themes'
import type { CompanySettings, BankConnection } from '@/types'
import { NotificationSettings } from '@/extensions/general/push-notifications/NotificationSettings'
import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings'
import { BankSelector, type Bank } from '@/extensions/general/enable-banking/components/BankSelector'
import { BankConnectionStatus } from '@/extensions/general/enable-banking/components/BankConnectionStatus'
export default function SettingsPage() {
const router = useRouter()
@@ -46,9 +44,12 @@ export default function SettingsPage() {
const [isSyncing, setIsSyncing] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const [hasBankingExtension, setHasBankingExtension] = useState(false)
const [selectedBank, setSelectedBank] = useState<Bank | null>(null)
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const initialTab = searchParams.get('tab') || 'company'
useEffect(() => {
setMounted(true)
}, [])
@@ -106,18 +107,16 @@ export default function SettingsPage() {
setBankConnections(connections || [])
// Check if extension API is available (will return banks list if extension is loaded)
// Check if Enable Banking extension is enabled via toggle API
try {
const bankingCheck = await fetch('/api/extensions/enable-banking/callback', {
method: 'HEAD',
})
// The extension route existing means it's deployed - but we also need
// to check if there are existing connections to show the tab
setHasBankingExtension(
(connections && connections.length > 0) || bankingCheck.status !== 404
)
const toggleRes = await fetch('/api/extensions/toggles/general/enable-banking')
if (toggleRes.ok) {
const { data } = await toggleRes.json()
setHasBankingExtension(data?.enabled || (connections && connections.length > 0) || false)
} else {
setHasBankingExtension((connections && connections.length > 0) || false)
}
} catch {
// If the extension routes don't exist, only show tab if there are existing connections
setHasBankingExtension((connections && connections.length > 0) || false)
}
@@ -182,7 +181,7 @@ export default function SettingsPage() {
setIsConnecting(true)
try {
const response = await fetch('/api/extensions/enable-banking/callback', {
const response = await fetch('/api/extensions/ext/enable-banking/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aspsp_name: bankName, aspsp_country: bankCountry }),
@@ -210,7 +209,7 @@ export default function SettingsPage() {
setIsSyncing(true)
try {
const response = await fetch('/api/extensions/enable-banking/sync/cron', {
const response = await fetch('/api/extensions/ext/enable-banking/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connection_id: connectionId }),
@@ -275,24 +274,6 @@ export default function SettingsPage() {
const activeConnections = bankConnections.filter((c) => c.status === 'active')
// Helper to calculate days until consent expires
function getDaysUntilExpiry(expiresAt: string | null): number | null {
if (!expiresAt) return null
const expiryDate = new Date(expiresAt)
const now = new Date()
const diffTime = expiryDate.getTime() - now.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return Math.max(0, diffDays)
}
function isConsentExpiringSoon(expiresAt: string | null): boolean {
if (!expiresAt) return false
const expiryDate = new Date(expiresAt)
const warningDate = new Date()
warningDate.setDate(warningDate.getDate() + 7)
return expiryDate <= warningDate
}
return (
<div className="space-y-6">
<div>
@@ -302,7 +283,7 @@ export default function SettingsPage() {
</p>
</div>
<Tabs defaultValue="company" className="space-y-6">
<Tabs defaultValue={initialTab} className="space-y-6">
<TabsList className="flex-wrap h-auto gap-1">
<TabsTrigger value="company">
<Building className="mr-2 h-4 w-4" />
@@ -554,66 +535,63 @@ export default function SettingsPage() {
<CardTitle>Anslutna banker</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{activeConnections.map((connection) => {
const daysUntilExpiry = getDaysUntilExpiry(connection.consent_expires)
const isExpiring = isConsentExpiringSoon(connection.consent_expires)
return (
<div
key={connection.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<CreditCard className="h-5 w-5 text-primary" />
</div>
<div>
<p className="font-medium">{connection.bank_name}</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
Senast synkad: {formatDate(connection.last_synced_at || connection.created_at)}
</span>
{isExpiring && (
<Badge variant="warning" className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{daysUntilExpiry} dagar kvar
</Badge>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSyncTransactions(connection.id)}
disabled={isSyncing}
>
{isSyncing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDisconnectBank(connection.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
)
})}
{activeConnections.map((connection) => (
<BankConnectionStatus
key={connection.id}
connection={connection}
onSync={handleSyncTransactions}
onDisconnect={handleDisconnectBank}
isSyncing={isSyncing}
/>
))}
</CardContent>
</Card>
)}
{/* Connect new bank */}
<Card>
<CardHeader>
<CardTitle>Anslut ny bank</CardTitle>
<CardDescription>
Välj din bank nedan för att koppla ditt konto via PSD2.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<BankSelector
onSelect={setSelectedBank}
isLoading={isConnecting}
/>
{selectedBank && (
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
<div className="flex items-center gap-3">
<Landmark className="h-5 w-5 text-primary" />
<div>
<p className="text-sm font-medium">{selectedBank.name}</p>
<p className="text-xs text-muted-foreground">{selectedBank.country}</p>
</div>
</div>
<Button
onClick={() => handleConnectBank(selectedBank.name, selectedBank.country)}
disabled={isConnecting}
>
{isConnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Ansluter...
</>
) : (
'Anslut bank'
)}
</Button>
</div>
)}
</CardContent>
</Card>
{/* Info about PSD2 */}
<Card>
<CardHeader>
<CardTitle>Bankintegration (PSD2)</CardTitle>
<CardTitle>Om bankintegration (PSD2)</CardTitle>
<CardDescription>
Automatisk import av transaktioner via PSD2 open banking.
Samtycket gäller i 90 dagar och behöver sedan förnyas.
+7 -4
View File
@@ -76,11 +76,14 @@ export default function DashboardNav({ companyName, entityType, enabledExtension
const isOvrigtExpanded = isOnOvrigtPage || manualOvrigtExpanded
// Auto-expand Tillägg when on an extension page, or when manually toggled (persisted)
const isOnExtensionPage = pathname.startsWith('/e/')
const [manualTillaggExpanded, setManualTillaggExpanded] = useState(() => {
if (typeof window === 'undefined') return false
return localStorage.getItem('tillagg-expanded') === 'true'
})
const [manualTillaggExpanded, setManualTillaggExpanded] = useState(false)
const isTillaggExpanded = isOnExtensionPage || manualTillaggExpanded
// Restore persisted state after hydration to avoid SSR mismatch
useEffect(() => {
const stored = localStorage.getItem('tillagg-expanded') === 'true'
if (stored) setManualTillaggExpanded(true)
}, [])
const [liveExtensions, setLiveExtensions] = useState(enabledExtensions ?? [])
const fetchExtensions = useCallback(async () => {
@@ -1,23 +1,71 @@
'use client'
import { useState } from 'react'
import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { useExtensionToggle } from '@/lib/extensions/hooks'
export default function ExtensionToggleButton({
sectorSlug,
extensionSlug,
subscriptionNotice,
}: {
sectorSlug: string
extensionSlug: string
subscriptionNotice?: string
}) {
const { enabled, isLoading, toggle } = useExtensionToggle(sectorSlug, extensionSlug)
const [showConfirm, setShowConfirm] = useState(false)
function handleToggle() {
// Show confirmation dialog when enabling an extension with a subscription notice
if (!enabled && subscriptionNotice) {
setShowConfirm(true)
return
}
toggle()
}
function handleConfirm() {
setShowConfirm(false)
toggle()
}
return (
<Switch
checked={enabled}
onCheckedChange={toggle}
disabled={isLoading}
aria-label={enabled ? 'Inaktivera tillägg' : 'Aktivera tillägg'}
/>
<>
<Switch
checked={enabled}
onCheckedChange={handleToggle}
disabled={isLoading}
aria-label={enabled ? 'Inaktivera tillägg' : 'Aktivera tillägg'}
/>
{subscriptionNotice && (
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>Extern prenumeration krävs</DialogTitle>
<DialogDescription>{subscriptionNotice}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setShowConfirm(false)}>
Avbryt
</Button>
<Button onClick={handleConfirm}>
Jag förstår, aktivera
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
)
}
@@ -1,15 +1,24 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { Landmark } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Landmark, Settings } from 'lucide-react'
import Link from 'next/link'
export default function EnableBankingWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="Bankintegration (PSD2)"
description="Koppla ditt bankkonto under Inst\u00e4llningar f\u00f6r att synka transaktioner automatiskt."
icon={<Landmark className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
<div className="flex flex-col items-center justify-center py-16 text-center">
<Landmark className="h-12 w-12 text-muted-foreground/40 mb-4" />
<h3 className="text-lg font-medium text-foreground">Bankintegration (PSD2)</h3>
<p className="text-sm text-muted-foreground mt-1 max-w-md">
Koppla ditt bankkonto under Inställningar för att synka transaktioner automatiskt.
</p>
<Button asChild variant="outline" className="mt-4">
<Link href="/settings?tab=banking">
<Settings className="mr-2 h-4 w-4" />
Gå till bankinställningar
</Link>
</Button>
</div>
)
}
+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)
}