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:
@@ -9,6 +9,7 @@ import {
|
||||
findCounterpartyTemplate,
|
||||
buildMappingResultFromCounterpartyTemplate,
|
||||
} from './counterparty-templates'
|
||||
import { detectOwnAccountTransfer } from './own-account-detector'
|
||||
import type {
|
||||
MappingRule,
|
||||
MappingResult,
|
||||
@@ -56,6 +57,33 @@ export async function evaluateMappingRules(
|
||||
): Promise<MappingResult> {
|
||||
const bankAccount = settlementAccount || '1930'
|
||||
|
||||
// Pre-step: detect intra-company transfers. When the counterparty IBAN
|
||||
// matches another cash_accounts row for the same company, book both legs
|
||||
// as a transfer between the two ledger accounts instead of running the
|
||||
// priority rules (which would mis-categorize the outflow as an expense).
|
||||
try {
|
||||
const transfer = await detectOwnAccountTransfer(supabase, companyId, transaction)
|
||||
if (transfer) {
|
||||
const isFx =
|
||||
(transaction.currency || '').toUpperCase() !==
|
||||
(transfer.counterCurrency || '').toUpperCase()
|
||||
return buildOwnAccountTransferResult(
|
||||
transaction,
|
||||
bankAccount,
|
||||
transfer.counterLedgerAccount,
|
||||
isFx,
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal — falling through to normal categorization is correct when
|
||||
// the detector fails. We log so an unexpected upstream error is visible.
|
||||
log.warn('own-account transfer detection failed', {
|
||||
companyId,
|
||||
transactionId: transaction.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch all active rules (user-specific + system defaults), ordered by priority
|
||||
const { data: rules, error } = await supabase
|
||||
.from('mapping_rules')
|
||||
@@ -299,6 +327,48 @@ function getDefaultResult(transaction: Transaction, bankAccount = '1930'): Mappi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a MappingResult for a detected own-account transfer.
|
||||
*
|
||||
* For an outflow (negative amount): debit the counter account, credit this
|
||||
* side's settlement account. The counter side will book the mirror entry when
|
||||
* its row is ingested.
|
||||
*
|
||||
* For an inflow (positive amount): debit this side's settlement account,
|
||||
* credit the counter account.
|
||||
*
|
||||
* Confidence is high (0.95) because IBAN match against the company's own
|
||||
* cash_accounts is an exact identity check, not a heuristic.
|
||||
*
|
||||
* `isFx` flips `requires_review` to true when the two legs sit on different
|
||||
* currencies (e.g. SEK 1930 → EUR 1932). A cross-currency leg generally
|
||||
* realises a kursvinst/kursförlust on 3960/7960 (ÅRL 4 kap 10 §) that the
|
||||
* two-line transfer entry doesn't capture — a human must confirm the FX gain
|
||||
* or loss line rather than auto-booking a potentially incomplete entry.
|
||||
* Same-currency transfers stay auto-bookable.
|
||||
*/
|
||||
function buildOwnAccountTransferResult(
|
||||
transaction: Transaction,
|
||||
bankAccount: string,
|
||||
counterAccount: string,
|
||||
isFx: boolean = false,
|
||||
): MappingResult {
|
||||
const isOutflow = transaction.amount < 0
|
||||
return {
|
||||
rule: null,
|
||||
debit_account: isOutflow ? counterAccount : bankAccount,
|
||||
credit_account: isOutflow ? bankAccount : counterAccount,
|
||||
risk_level: isFx ? 'MEDIUM' : 'LOW',
|
||||
confidence: isFx ? 0.7 : 0.95,
|
||||
requires_review: isFx,
|
||||
default_private: false,
|
||||
vat_lines: [],
|
||||
description: isFx
|
||||
? 'Överföring mellan egna konton (FX — granska kursvinst/förlust)'
|
||||
: 'Överföring mellan egna konton',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace any default 1930 references in a mapping result with the actual settlement account.
|
||||
* This allows mapping rules and templates that don't explicitly set a bank account
|
||||
|
||||
Reference in New Issue
Block a user