feat(validation): add SKR standard validation for account compliance
This commit is contained in:
@@ -9,6 +9,14 @@ import type {
|
||||
IJournalEntryLine,
|
||||
IAccountBalance,
|
||||
} from './skr.types.js';
|
||||
import { SKR03_ACCOUNTS } from './skr03.data.js';
|
||||
import { SKR04_ACCOUNTS } from './skr04.data.js';
|
||||
|
||||
// Module-level Maps for O(1) SKR standard lookups
|
||||
const STANDARD_SKR_MAP = {
|
||||
SKR03: new Map(SKR03_ACCOUNTS.map(a => [a.accountNumber, a])),
|
||||
SKR04: new Map(SKR04_ACCOUNTS.map(a => [a.accountNumber, a])),
|
||||
};
|
||||
|
||||
export class Ledger {
|
||||
private logger: plugins.smartlog.Smartlog;
|
||||
@@ -81,6 +89,12 @@ export class Ledger {
|
||||
const accountNumbers = journalData.lines.map((line) => line.accountNumber);
|
||||
await this.validateAccounts(accountNumbers);
|
||||
|
||||
// Validate against SKR standard (warnings only by default)
|
||||
await this.validateAccountsAgainstSKR(journalData.lines, {
|
||||
strict: false, // Start with warnings only
|
||||
warnOnNameMismatch: false // Names vary, don't spam logs
|
||||
});
|
||||
|
||||
// Validate journal entry is balanced
|
||||
this.validateJournalBalance(journalData.lines);
|
||||
|
||||
@@ -139,6 +153,77 @@ export class Ledger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate accounts against SKR standard data
|
||||
*/
|
||||
private async validateAccountsAgainstSKR(
|
||||
lines: IJournalEntryLine[],
|
||||
options?: { strict?: boolean; warnOnNameMismatch?: boolean }
|
||||
): Promise<void> {
|
||||
const { strict = false, warnOnNameMismatch = false } = options || {};
|
||||
const skrMap = STANDARD_SKR_MAP[this.skrType];
|
||||
|
||||
if (!skrMap) {
|
||||
this.logger.log('warn', `No SKR standard map available for ${this.skrType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueAccountNumbers = [...new Set(lines.map(line => line.accountNumber))];
|
||||
|
||||
for (const accountNumber of uniqueAccountNumbers) {
|
||||
const standardAccount = skrMap.get(accountNumber);
|
||||
|
||||
if (!standardAccount) {
|
||||
// Special case: SKR04 class 8 is designated for custom accounts ("frei")
|
||||
if (this.skrType === 'SKR04' && accountNumber.startsWith('8')) {
|
||||
this.logger.log('debug', `Account ${accountNumber} is in SKR04 class 8 (custom accounts allowed)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = `Account ${accountNumber} is not a standard ${this.skrType} account`;
|
||||
if (strict) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
this.logger.log('warn', message);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get actual account from database to compare
|
||||
const dbAccount = await Account.getAccountByNumber(accountNumber, this.skrType);
|
||||
if (!dbAccount) {
|
||||
// Account doesn't exist in DB, will be caught by validateAccounts()
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate type and class match SKR standard
|
||||
if (dbAccount.accountType !== standardAccount.accountType) {
|
||||
const message = `Account ${accountNumber} type mismatch: expected '${standardAccount.accountType}', got '${dbAccount.accountType}'`;
|
||||
if (strict) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
this.logger.log('warn', message);
|
||||
}
|
||||
}
|
||||
|
||||
if (dbAccount.accountClass !== standardAccount.accountClass) {
|
||||
const message = `Account ${accountNumber} class mismatch: expected ${standardAccount.accountClass}, got ${dbAccount.accountClass}`;
|
||||
if (strict) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
this.logger.log('warn', message);
|
||||
}
|
||||
}
|
||||
|
||||
// Warn on name mismatch (common and acceptable in practice)
|
||||
if (warnOnNameMismatch && dbAccount.accountName !== standardAccount.accountName) {
|
||||
this.logger.log('info',
|
||||
`Account ${accountNumber} name differs from SKR standard: '${dbAccount.accountName}' vs '${standardAccount.accountName}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse a transaction
|
||||
*/
|
||||
|
Reference in New Issue
Block a user