Fix/vat parent accounts (#438)

* feat(enable-banking): add support for account selection and syncing

- Updated StoredAccount interface to include an 'enabled' flag for account syncing preferences.
- Enhanced ensureFiscalPeriod function to handle overlapping fiscal periods with posted entries and opening balances.
- Added tests for fiscal period validation and account syncing logic.
- Implemented AccountPickerDialog component for user account selection.
- Created API routes for PATCH /accounts and POST /sync to manage account syncing.
- Introduced 'pending_selection' status for bank connections to allow user account selection before syncing.
- Updated database migration to support new connection status and backfill existing accounts with enabled=true.

* feat(enable-banking): implement account selection and consent event logging

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-11 17:12:00 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent d0fbc2b616
commit a53a119a2e
20 changed files with 1761 additions and 112 deletions
@@ -9,6 +9,7 @@ import {
CreditCard,
AlertTriangle,
RefreshCw,
Settings,
Trash2,
Loader2,
CheckCircle,
@@ -22,6 +23,7 @@ interface BankConnectionStatusProps {
onSync: (connectionId: string) => void
onDisconnect: (connectionId: string) => void
onReconnect?: (bank: { name: string; country: string }) => void
onManageAccounts?: (connectionId: string) => void
isSyncing?: boolean
}
@@ -30,6 +32,7 @@ export function BankConnectionStatus({
onSync,
onDisconnect,
onReconnect,
onManageAccounts,
isSyncing = false,
}: BankConnectionStatusProps) {
const daysUntilExpiry = getDaysUntilExpiry(connection.consent_expires)
@@ -86,8 +89,11 @@ export function BankConnectionStatus({
currency: string
balance?: number
balance_updated_at?: string
enabled?: boolean
}>) || []
const enabledCount = accounts.filter((a) => a.enabled !== false).length
const [now] = useState(() => Date.now())
function formatBalanceAge(updatedAt: string): string {
@@ -168,6 +174,16 @@ export function BankConnectionStatus({
)}
</Button>
)}
{onManageAccounts && (
<Button
variant="ghost"
size="sm"
onClick={() => onManageAccounts(connection.id)}
title="Hantera konton"
>
<Settings className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
@@ -228,40 +244,55 @@ export function BankConnectionStatus({
{/* Accounts list */}
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Konton</p>
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">Konton</p>
<p className="text-xs text-muted-foreground tabular-nums">
{enabledCount} av {accounts.length} synkas
</p>
</div>
<div className="space-y-2">
{accounts.map((account) => (
<div
key={account.uid}
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
>
<div>
<p className="text-sm font-medium">
{account.name || account.iban || 'Okänt konto'}
</p>
{account.iban && (
<p className="text-xs text-muted-foreground">
{account.iban.replace(/(.{4})/g, '$1 ').trim()}
</p>
)}
</div>
{account.balance !== undefined && (
<div className="text-right">
<p className="text-sm font-medium">
{new Intl.NumberFormat('sv-SE', {
style: 'currency',
currency: account.currency,
}).format(account.balance)}
</p>
{account.balance_updated_at && (
<p className="text-[10px] text-muted-foreground">
{formatBalanceAge(account.balance_updated_at)}
{accounts.map((account) => {
const isDisabled = account.enabled === false
return (
<div
key={account.uid}
className={`flex items-center justify-between p-3 rounded-lg ${isDisabled ? 'bg-muted/20 opacity-60' : 'bg-muted/50'}`}
>
<div>
<div className="flex items-center gap-2">
<p className="text-sm font-medium">
{account.name || account.iban || 'Okänt konto'}
</p>
{isDisabled && (
<span className="text-[10px] uppercase tracking-wide text-muted-foreground border border-border rounded px-1.5 py-0.5">
Synkas ej
</span>
)}
</div>
{account.iban && (
<p className="text-xs text-muted-foreground">
{account.iban.replace(/(.{4})/g, '$1 ').trim()}
</p>
)}
</div>
)}
</div>
))}
{account.balance !== undefined && (
<div className="text-right">
<p className="text-sm font-medium tabular-nums">
{new Intl.NumberFormat('sv-SE', {
style: 'currency',
currency: account.currency,
}).format(account.balance)}
</p>
{account.balance_updated_at && (
<p className="text-[10px] text-muted-foreground">
{formatBalanceAge(account.balance_updated_at)}
</p>
)}
</div>
)}
</div>
)
})}
</div>
</div>
)}