Files
accounted/extensions/general/enable-banking/lib/sync.ts
T
Mattsson ade4ad5971 Fiscal period and multi bank (#228)
* feat: add fiscal period backward chaining and entry date validation

Support creating fiscal periods before the earliest existing period
(backward chaining) for backfill scenarios, alongside the existing
forward chaining. The engine now validates that entry dates fall within
the selected fiscal period, with a Swedish error message. The journal
entry form auto-selects the matching period and shows a warning with
a CreatePeriodDialog when no period covers the entry date.


* feat: support multi-bank-account for imports and reconciliation

Plumb a configurable settlement account through the entire bank import
pipeline — mapping engine, transaction entries, ingest, and
reconciliation — so secondary bank accounts (e.g. 1931, 1932) work
correctly instead of hardcoding 1930. Adds a get_unlinked_bank_lines
RPC that generalizes the existing get_unlinked_1930_lines with a
fallback for backwards compatibility. The bank file import UI now shows
a bank account selector when multiple 19xx accounts exist. Also adds
default_vat_code/sru_code to account creation and fixes uploadDocument
argument order in enable-banking sync.
2026-04-13 11:13:02 +02:00

130 lines
4.4 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import { getAllTransactionsWithRaw, convertTransaction, getAccountBalance } from './api-client'
import { uploadDocument } from '@/lib/core/documents/document-service'
import { ingestTransactions as defaultIngest } from '@/lib/transactions/ingest'
import type { RawTransaction, IngestResult, IngestOptions } from '@/types'
import type { StoredAccount } from '../types'
/** Ingest function signature — matches lib/transactions/ingest */
export type IngestFn = (
supabase: SupabaseClient,
companyId: string,
userId: string,
raw: RawTransaction[],
options?: IngestOptions
) => Promise<IngestResult>
export interface SyncOptions {
/** Skip auto-categorization during ingestion (e.g. SIE overlap) */
skipAutoCategorization?: boolean
}
export interface SyncResult {
imported: number
duplicates: number
errors: number
}
/**
* Sync transactions for a single bank account via Enable Banking PSD2.
*
* Fetches transactions from the Enable Banking API, converts to RawTransaction
* format, and delegates to the shared ingestion pipeline. Raw API responses
* are archived as räkenskapsinformation per BFL 7 kap.
*
* @param ingest - Optional ingest function override (defaults to core ingestTransactions).
* When called from an extension handler with ctx.services.ingestTransactions,
* pass that function to avoid direct @/lib imports.
*/
export async function syncAccountTransactions(
supabase: SupabaseClient,
companyId: string,
userId: string,
connectionId: string,
account: StoredAccount,
fromDate: string,
toDate: string,
ingest: IngestFn = defaultIngest,
syncOptions?: SyncOptions
): Promise<SyncResult> {
console.log('[enable-banking] syncAccountTransactions starting', {
connectionId,
accountUid: account.uid,
accountIban: account.iban,
fromDate,
toDate,
})
const { transactions, rawPages } = await getAllTransactionsWithRaw(
account.uid,
fromDate,
toDate,
)
console.log('[enable-banking] Fetched transactions from API', {
connectionId,
accountUid: account.uid,
transactionCount: transactions.length,
rawPageCount: rawPages.length,
})
const bankTransactions = transactions.map(tx => convertTransaction(tx, account.currency))
// Convert Enable Banking format to generic RawTransaction
const rawTransactions: RawTransaction[] = bankTransactions.map((tx) => ({
date: tx.booking_date || tx.date,
description: tx.description || tx.counterparty_name || 'Unknown',
amount: tx.amount,
currency: tx.currency || account.currency,
external_id: `eb_${account.iban || account.uid}_${tx.id}`,
mcc_code: tx.merchant_category_code ? parseInt(tx.merchant_category_code, 10) : null,
merchant_name: tx.counterparty_name || null,
reference: tx.reference || null,
bank_connection_id: connectionId,
import_source: 'enable_banking',
}))
const ingestOptions: IngestOptions | undefined = syncOptions?.skipAutoCategorization
? { skipAutoCategorization: true }
: undefined
const ingestResult = await ingest(supabase, companyId, userId, rawTransactions, ingestOptions)
console.log('[enable-banking] Ingest result', {
connectionId,
accountUid: account.uid,
imported: ingestResult.imported,
duplicates: ingestResult.duplicates,
errors: ingestResult.errors,
})
// Archive raw PSD2 API responses as räkenskapsinformation (BFL 7 kap)
for (let i = 0; i < rawPages.length; i++) {
try {
const fileName = `psd2-response_${connectionId}_${account.uid}_${new Date().toISOString().replace(/[:.]/g, '-')}_p${i + 1}.json`
const buffer = new TextEncoder().encode(rawPages[i]).buffer as ArrayBuffer
await uploadDocument(supabase, userId, companyId,
{ name: fileName, buffer, type: 'application/json' },
{ upload_source: 'api' }
)
} catch (archiveError) {
console.error(`[enable-banking] Failed to archive raw response page ${i + 1}:`, archiveError)
// Archival failure must not fail the sync
}
}
// Update account balance
try {
const balance = await getAccountBalance(account.uid)
account.balance = balance.amount
account.balance_updated_at = new Date().toISOString()
} catch {
// Keep previous balance, don't update timestamp
}
return {
imported: ingestResult.imported,
duplicates: ingestResult.duplicates,
errors: ingestResult.errors,
}
}