feat: implement skattekonto drift detection and alerting (#525)
* feat: implement skattekonto drift detection and alerting - Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum. - Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming. - Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows. feat: create own account transfer detection - Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN. - Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs. feat: establish cash accounts as a first-class entity - Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures. - Implement functions for listing, upserting, and managing cash accounts, including primary account designation. feat: enhance GL line reconciliation functionality - Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies. - Update related functions to ensure compatibility with the new cash_accounts structure. feat: capture counterparty IBAN in transactions - Add counterparty_iban column to transactions table to facilitate intra-account transfer detection. - Create index for efficient lookups based on counterparty IBAN. * feat: Enhance cash account handling and reconciliation processes - Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'. - Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes. - Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy. - Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731). - Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities. - Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates. - Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one. - Updated email notifications for drift detection to avoid exposing sensitive financial data. - Enhanced bank reconciliation logic to handle multi-currency transactions correctly. - Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage. - Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards.
This commit is contained in:
@@ -3,6 +3,21 @@ import { NextResponse } from 'next/server'
|
||||
import { createSession, type AccountInfo } from '@/extensions/general/enable-banking/lib/api-client'
|
||||
import type { StoredAccount } from '@/extensions/general/enable-banking/types'
|
||||
import { eventBus } from '@/lib/events/bus'
|
||||
import { upsertFromPsd2 } from '@/lib/cash-accounts/service'
|
||||
|
||||
// Suggested BAS account per currency. Mirrors the AccountPickerDialog defaults
|
||||
// (SEK→1930, EUR→1932, USD→1933, GBP→1934). The user can re-map in the picker
|
||||
// after this callback redirects them.
|
||||
const CURRENCY_DEFAULTS: Record<string, string> = {
|
||||
SEK: '1930',
|
||||
EUR: '1932',
|
||||
USD: '1933',
|
||||
GBP: '1934',
|
||||
}
|
||||
|
||||
function defaultLedgerForCurrency(currency: string): string {
|
||||
return CURRENCY_DEFAULTS[currency.toUpperCase()] ?? '1930'
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/extensions/enable-banking/callback
|
||||
@@ -170,6 +185,56 @@ export async function GET(request: Request) {
|
||||
throw new Error(`Failed to update connection: ${updateError.message}`)
|
||||
}
|
||||
|
||||
// Mirror each PSD2 account into cash_accounts so routing decisions read from
|
||||
// the canonical entity table. The user picks a ledger_account in the
|
||||
// AccountPickerDialog after this redirect; until then we route SEK→1930,
|
||||
// EUR→1932, USD→1933, GBP→1934 by convention.
|
||||
for (const account of accountsMetadata) {
|
||||
const targetLedger = defaultLedgerForCurrency(account.currency)
|
||||
try {
|
||||
await upsertFromPsd2(supabase, updatedConnection.company_id, {
|
||||
bank_connection_id: updatedConnection.id,
|
||||
external_uid: account.uid,
|
||||
currency: account.currency,
|
||||
ledger_account: targetLedger,
|
||||
iban: account.iban ?? null,
|
||||
name: account.name ?? null,
|
||||
enabled: account.enabled ?? true,
|
||||
})
|
||||
} catch (cashErr) {
|
||||
const reason = cashErr instanceof Error ? cashErr.message : String(cashErr)
|
||||
console.error('[enable-banking] Failed to mirror cash_account on callback', {
|
||||
connectionId: updatedConnection.id,
|
||||
uid: account.uid,
|
||||
error: reason,
|
||||
})
|
||||
// Persist the failure to event_log so a security review can see that
|
||||
// a PSD2 account returned by the bank was not mirrored into our
|
||||
// routing table — otherwise this is only visible in console output
|
||||
// (ASVS V16 / ISO 27001 A.8.15 / SOC 2 CC7.2).
|
||||
try {
|
||||
await eventBus.emit({
|
||||
type: 'bank_connection.cash_account_mirror_failed',
|
||||
payload: {
|
||||
connectionId: updatedConnection.id,
|
||||
bankName: updatedConnection.bank_name ?? null,
|
||||
accountUid: account.uid,
|
||||
ledgerAccount: targetLedger,
|
||||
currency: account.currency,
|
||||
reason,
|
||||
userId: updatedConnection.user_id,
|
||||
companyId: updatedConnection.company_id,
|
||||
},
|
||||
})
|
||||
} catch (emitError) {
|
||||
console.error('[enable-banking] Failed to emit cash_account_mirror_failed event', {
|
||||
connectionId: updatedConnection.id,
|
||||
error: emitError instanceof Error ? emitError.message : String(emitError),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audit trail: PSD2 consent has been exchanged and account metadata stored.
|
||||
// ASVS V16 requires this transition to be logged as a security event; emit
|
||||
// here so the event_log handler persists it (30-day TTL).
|
||||
|
||||
Reference in New Issue
Block a user