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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user