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:
Mattsson
2026-05-19 16:10:18 +02:00
committed by GitHub
parent e211ab31be
commit 8a6ce7093e
42 changed files with 3420 additions and 206 deletions
+13 -10
View File
@@ -1,11 +1,11 @@
/**
* pg-real test for get_unlinked_1930_lines (PR 3 of erp-mafia/gnubok#443).
* pg-real test for get_unlinked_gl_lines.
*
* Verifies the RPC excludes opening_balance vouchers from the unmatched-1930
* set, while preserving the existing behavior for posted bank-import vouchers,
* date-range filtering, and company scoping.
*
* Migration: 20260514132534_unlinked_1930_lines_exclude_opening_balance.sql
* date-range filtering, and company scoping. The RPC was renamed from
* get_unlinked_1930_lines in 20260519120000_get_unlinked_gl_lines.sql; the
* default p_account_number of '1930' keeps these assertions valid.
*/
import { describe, it, expect } from 'vitest'
import { randomUUID } from 'node:crypto'
@@ -55,7 +55,7 @@ async function insertPostedJournalEntry(params: {
return id
}
describe('get_unlinked_1930_lines RPC — opening_balance exclusion', () => {
describe('get_unlinked_gl_lines RPC — opening_balance exclusion', () => {
it('excludes opening_balance vouchers from the unmatched-1930 set', async () => {
const userId = await insertAuthUser()
const companyId = await insertCompany({ createdBy: userId })
@@ -93,7 +93,7 @@ describe('get_unlinked_1930_lines RPC — opening_balance exclusion', () => {
})
const { rows } = await getPool().query(
`SELECT journal_entry_id, source_type FROM public.get_unlinked_1930_lines($1)`,
`SELECT journal_entry_id, source_type FROM public.get_unlinked_gl_lines($1)`,
[companyId],
)
@@ -127,9 +127,12 @@ describe('get_unlinked_1930_lines RPC — opening_balance exclusion', () => {
voucherNumber: 11,
})
// Window covers only the second voucher.
// Window covers only the second voucher. Use named notation so we don't have
// to repeat the '1930' default just to reach the date params.
const { rows } = await getPool().query(
`SELECT entry_date FROM public.get_unlinked_1930_lines($1, $2, $3) ORDER BY entry_date`,
`SELECT entry_date FROM public.get_unlinked_gl_lines(
p_company_id => $1, p_date_from => $2, p_date_to => $3
) ORDER BY entry_date`,
[companyId, '2026-07-01', '2026-12-31'],
)
@@ -155,11 +158,11 @@ describe('get_unlinked_1930_lines RPC — opening_balance exclusion', () => {
})
const { rows: rowsA } = await getPool().query(
`SELECT 1 FROM public.get_unlinked_1930_lines($1)`,
`SELECT 1 FROM public.get_unlinked_gl_lines($1)`,
[companyA],
)
const { rows: rowsB } = await getPool().query(
`SELECT 1 FROM public.get_unlinked_1930_lines($1)`,
`SELECT 1 FROM public.get_unlinked_gl_lines($1)`,
[companyB],
)
expect(rowsA).toHaveLength(1)