feat(core): initial release of SKR03/SKR04 German accounting standards implementation
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped

- Complete implementation of German standard charts of accounts
- SKR03 (Process Structure Principle) for trading/service companies
- SKR04 (Financial Classification Principle) for manufacturing companies
- Double-entry bookkeeping with MongoDB persistence
- Comprehensive reporting suite with DATEV export
- Full TypeScript support and type safety
This commit is contained in:
2025-08-09 12:00:40 +00:00
commit 8a9056e767
31 changed files with 16560 additions and 0 deletions

78
test/test.basic.ts Normal file
View File

@@ -0,0 +1,78 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as skr from '../ts/index.js';
tap.test('should export all required classes and types', async () => {
expect(skr.Account).toBeTypeOf('function');
expect(skr.Transaction).toBeTypeOf('function');
expect(skr.JournalEntry).toBeTypeOf('function');
expect(skr.ChartOfAccounts).toBeTypeOf('function');
expect(skr.Ledger).toBeTypeOf('function');
expect(skr.Reports).toBeTypeOf('function');
expect(skr.SkrApi).toBeTypeOf('function');
expect(skr.SKR03_ACCOUNTS).toBeArray();
expect(skr.SKR04_ACCOUNTS).toBeArray();
});
tap.test('should have correct number of SKR03 accounts', async () => {
expect(skr.SKR03_ACCOUNTS.length).toBeGreaterThan(50);
expect(skr.SKR03_ACCOUNTS[0].skrType).toEqual('SKR03');
});
tap.test('should have correct number of SKR04 accounts', async () => {
expect(skr.SKR04_ACCOUNTS.length).toBeGreaterThan(50);
expect(skr.SKR04_ACCOUNTS[0].skrType).toEqual('SKR04');
});
tap.test('should have valid account structure for SKR03', async () => {
const firstAccount = skr.SKR03_ACCOUNTS[0];
expect(firstAccount.accountNumber).toBeTypeofString();
expect(firstAccount.accountNumber.length).toEqual(4);
expect(firstAccount.accountName).toBeTypeofString();
expect(firstAccount.accountClass).toBeTypeofNumber();
expect(firstAccount.accountType).toMatch(
/^(asset|liability|equity|revenue|expense)$/,
);
});
tap.test('should have valid account structure for SKR04', async () => {
const firstAccount = skr.SKR04_ACCOUNTS[0];
expect(firstAccount.accountNumber).toBeTypeofString();
expect(firstAccount.accountNumber.length).toEqual(4);
expect(firstAccount.accountName).toBeTypeofString();
expect(firstAccount.accountClass).toBeTypeofNumber();
expect(firstAccount.accountType).toMatch(
/^(asset|liability|equity|revenue|expense)$/,
);
});
tap.test('should have account classes 0-9 in SKR03', async () => {
const classes = new Set(skr.SKR03_ACCOUNTS.map((a) => a.accountClass));
expect(classes.size).toBeGreaterThan(5);
// Check that we have accounts in multiple classes
for (let i = 0; i <= 9; i++) {
const accountsInClass = skr.SKR03_ACCOUNTS.filter(
(a) => a.accountClass === i,
);
if (accountsInClass.length > 0) {
expect(accountsInClass[0].accountNumber[0]).toEqual(i.toString());
}
}
});
tap.test('should have account classes 0-9 in SKR04', async () => {
const classes = new Set(skr.SKR04_ACCOUNTS.map((a) => a.accountClass));
expect(classes.size).toBeGreaterThan(5);
// Check that we have accounts in multiple classes
for (let i = 0; i <= 9; i++) {
const accountsInClass = skr.SKR04_ACCOUNTS.filter(
(a) => a.accountClass === i,
);
if (accountsInClass.length > 0) {
expect(accountsInClass[0].accountNumber[0]).toEqual(i.toString());
}
}
});
export default tap.start();

159
test/test.skr03.ts Normal file
View File

@@ -0,0 +1,159 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as skr from '../ts/index.js';
let api: skr.SkrApi;
tap.test('should initialize SKR03 API', async () => {
api = new skr.SkrApi({
mongoDbUrl: 'mongodb://localhost:27017',
dbName: 'test_skr03',
});
await api.initialize('SKR03');
expect(api.getSKRType()).toEqual('SKR03');
});
tap.test('should have SKR03 accounts initialized', async () => {
const accounts = await api.listAccounts();
expect(accounts.length).toBeGreaterThan(50);
// Check specific SKR03 accounts exist
const kasse = await api.getAccount('1000');
expect(kasse).not.toBeNull();
expect(kasse.accountName).toEqual('Kasse');
expect(kasse.accountType).toEqual('asset');
const umsatz = await api.getAccount('4000');
expect(umsatz).not.toBeNull();
expect(umsatz.accountName).toEqual('Umsatzerlöse');
expect(umsatz.accountType).toEqual('revenue');
});
tap.test('should verify SKR03 process structure principle', async () => {
// SKR03 organizes accounts by business process
// Class 4: Operating Income
// Class 5: Material Costs
// Class 6: Personnel Costs
// Class 7: Other Operating Expenses
const class4 = await api.getAccountsByClass(4);
expect(class4.length).toBeGreaterThan(0);
expect(class4[0].accountType).toEqual('revenue');
const class5 = await api.getAccountsByClass(5);
expect(class5.length).toBeGreaterThan(0);
expect(class5[0].accountType).toEqual('expense');
const class6 = await api.getAccountsByClass(6);
expect(class6.length).toBeGreaterThan(0);
expect(class6[0].accountType).toEqual('expense');
});
tap.test('should create custom SKR03 account', async () => {
const customAccount = await api.createAccount({
accountNumber: '4999',
accountName: 'Custom Revenue Account',
accountClass: 4,
accountType: 'revenue',
description: 'Test custom account',
});
expect(customAccount.accountNumber).toEqual('4999');
expect(customAccount.skrType).toEqual('SKR03');
expect(customAccount.isActive).toBeTrue();
});
tap.test('should post transaction in SKR03', async () => {
const transaction = await api.postTransaction({
date: new Date(),
debitAccount: '1200', // Bank
creditAccount: '4000', // Revenue
amount: 1000,
description: 'Test sale',
reference: 'INV-001',
skrType: 'SKR03',
});
expect(transaction.status).toEqual('posted');
expect(transaction.amount).toEqual(1000);
expect(transaction.skrType).toEqual('SKR03');
});
tap.test('should post journal entry in SKR03', async () => {
const journalEntry = await api.postJournalEntry({
date: new Date(),
description: 'Test journal entry',
reference: 'JE-001',
lines: [
{ accountNumber: '1000', debit: 500 }, // Cash
{ accountNumber: '1200', debit: 500 }, // Bank
{ accountNumber: '4000', credit: 1000 }, // Revenue
],
skrType: 'SKR03',
});
expect(journalEntry.status).toEqual('posted');
expect(journalEntry.isBalanced).toBeTrue();
expect(journalEntry.totalDebits).toEqual(1000);
expect(journalEntry.totalCredits).toEqual(1000);
});
tap.test('should generate trial balance for SKR03', async () => {
const trialBalance = await api.generateTrialBalance();
expect(trialBalance.skrType).toEqual('SKR03');
expect(trialBalance.entries.length).toBeGreaterThan(0);
expect(trialBalance.isBalanced).toBeTrue();
expect(trialBalance.totalDebits).toEqual(trialBalance.totalCredits);
});
tap.test('should generate income statement for SKR03', async () => {
const incomeStatement = await api.generateIncomeStatement();
expect(incomeStatement.skrType).toEqual('SKR03');
expect(incomeStatement.revenue.length).toBeGreaterThanOrEqual(0);
expect(incomeStatement.expenses.length).toBeGreaterThanOrEqual(0);
expect(incomeStatement.netIncome).toEqual(
incomeStatement.totalRevenue - incomeStatement.totalExpenses,
);
});
tap.test('should generate balance sheet for SKR03', async () => {
const balanceSheet = await api.generateBalanceSheet();
expect(balanceSheet.skrType).toEqual('SKR03');
expect(balanceSheet.assets.totalAssets).toBeGreaterThanOrEqual(0);
expect(balanceSheet.isBalanced).toBeTrue();
});
tap.test('should search accounts in SKR03', async () => {
const results = await api.searchAccounts('Bank');
expect(results.length).toBeGreaterThan(0);
const bankAccount = results.find((a) => a.accountNumber === '1200');
expect(bankAccount).not.toBeNull();
});
tap.test('should export SKR03 accounts to CSV', async () => {
const csv = await api.exportAccountsToCSV();
expect(csv).toInclude('"Account";"Name";"Description";"Type";"Active"');
expect(csv).toInclude('1000');
expect(csv).toInclude('Kasse');
});
tap.test('should close API connection', async () => {
await api.close();
// Verify API requires reinitialization
let errorThrown = false;
try {
await api.listAccounts();
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('not initialized');
}
expect(errorThrown).toBeTrue();
});
export default tap.start();

194
test/test.skr04.ts Normal file
View File

@@ -0,0 +1,194 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as skr from '../ts/index.js';
let api: skr.SkrApi;
tap.test('should initialize SKR04 API', async () => {
api = new skr.SkrApi({
mongoDbUrl: 'mongodb://localhost:27017',
dbName: 'test_skr04',
});
await api.initialize('SKR04');
expect(api.getSKRType()).toEqual('SKR04');
});
tap.test('should have SKR04 accounts initialized', async () => {
const accounts = await api.listAccounts();
expect(accounts.length).toBeGreaterThan(50);
// Check specific SKR04 accounts exist
const kasse = await api.getAccount('1000');
expect(kasse).not.toBeNull();
expect(kasse.accountName).toEqual('Kasse');
expect(kasse.accountType).toEqual('asset');
const umsatz = await api.getAccount('4000');
expect(umsatz).not.toBeNull();
expect(umsatz.accountName).toEqual('Umsatzerlöse');
expect(umsatz.accountType).toEqual('revenue');
});
tap.test('should verify SKR04 financial classification principle', async () => {
// SKR04 organizes accounts by financial statement structure
// Class 2: Expenses Part 1
// Class 3: Expenses Part 2
// Class 4: Revenues Part 1
// Class 5: Revenues Part 2
const class2 = await api.getAccountsByClass(2);
expect(class2.length).toBeGreaterThan(0);
expect(class2[0].accountType).toEqual('expense');
const class3 = await api.getAccountsByClass(3);
expect(class3.length).toBeGreaterThan(0);
expect(class3[0].accountType).toEqual('expense');
const class4 = await api.getAccountsByClass(4);
expect(class4.length).toBeGreaterThan(0);
expect(class4[0].accountType).toEqual('revenue');
const class5 = await api.getAccountsByClass(5);
expect(class5.length).toBeGreaterThan(0);
expect(class5[0].accountType).toEqual('revenue');
});
tap.test('should handle Class 8 as free for use in SKR04', async () => {
// Class 8 in SKR04 is reserved for custom use
const class8 = await api.getAccountsByClass(8);
for (const account of class8) {
expect(account.accountName).toEqual('frei');
expect(account.description).toInclude('custom use');
}
});
tap.test('should post complex transaction in SKR04', async () => {
const transaction = await api.postTransaction({
date: new Date(),
debitAccount: '5400', // Goods with 19% VAT
creditAccount: '1600', // Trade payables
amount: 119,
description: 'Purchase with VAT',
reference: 'BILL-001',
skrType: 'SKR04',
vatAmount: 19,
});
expect(transaction.status).toEqual('posted');
expect(transaction.vatAmount).toEqual(19);
expect(transaction.skrType).toEqual('SKR04');
});
tap.test('should reverse transaction in SKR04', async () => {
// First create a transaction
const originalTransaction = await api.postTransaction({
date: new Date(),
debitAccount: '3000', // Rent expense
creditAccount: '1200', // Bank
amount: 500,
description: 'Rent payment',
reference: 'RENT-001',
skrType: 'SKR04',
});
// Then reverse it
const reversalTransaction = await api.reverseTransaction(
originalTransaction.id,
);
expect(reversalTransaction.reversalOf).toEqual(originalTransaction.id);
expect(reversalTransaction.debitAccount).toEqual(
originalTransaction.creditAccount,
);
expect(reversalTransaction.creditAccount).toEqual(
originalTransaction.debitAccount,
);
expect(reversalTransaction.amount).toEqual(originalTransaction.amount);
});
tap.test('should calculate correct balances in SKR04', async () => {
// Post several transactions
await api.postTransaction({
date: new Date(),
debitAccount: '1200', // Bank
creditAccount: '4300', // Revenue 19% VAT
amount: 1190,
description: 'Sale with VAT',
skrType: 'SKR04',
vatAmount: 190,
});
await api.postTransaction({
date: new Date(),
debitAccount: '2300', // Wages expense
creditAccount: '1200', // Bank
amount: 3000,
description: 'Salary payment',
skrType: 'SKR04',
});
// Check bank account balance
const bankBalance = await api.getAccountBalance('1200');
expect(bankBalance.accountNumber).toEqual('1200');
expect(bankBalance.balance).toEqual(
bankBalance.debitTotal - bankBalance.creditTotal,
);
});
tap.test('should export trial balance to CSV for SKR04', async () => {
const csv = await api.exportReportToCSV('trial_balance');
expect(csv).toInclude(
'"Account Number";"Account Name";"Debit";"Credit";"Balance"',
);
expect(csv).toInclude('TOTAL');
});
tap.test('should handle pagination for SKR04 accounts', async () => {
const page1 = await api.getAccountsPaginated(1, 10);
expect(page1.data.length).toBeLessThanOrEqual(10);
expect(page1.page).toEqual(1);
expect(page1.pageSize).toEqual(10);
expect(page1.total).toBeGreaterThan(50);
expect(page1.totalPages).toBeGreaterThan(5);
// Get second page
const page2 = await api.getAccountsPaginated(2, 10);
expect(page2.data[0].accountNumber).not.toEqual(page1.data[0].accountNumber);
});
tap.test('should validate double-entry rules', async () => {
const isValid1 = api.validateDoubleEntry(100, 100);
expect(isValid1).toBeTrue();
const isValid2 = api.validateDoubleEntry(100, 99);
expect(isValid2).toBeFalse();
const isValid3 = api.validateDoubleEntry(100.0, 100.001);
expect(isValid3).toBeTrue(); // Small rounding differences are acceptable
});
tap.test('should generate DATEV export for SKR04', async () => {
const datevExport = await api.exportToDATEV();
expect(datevExport).toInclude('EXTF');
expect(datevExport).toInclude('Buchungsstapel');
});
tap.test('should close API connection', async () => {
await api.close();
// Verify API requires reinitialization
let errorThrown = false;
try {
await api.listAccounts();
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('not initialized');
}
expect(errorThrown).toBeTrue();
});
export default tap.start();

276
test/test.transactions.ts Normal file
View File

@@ -0,0 +1,276 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as skr from '../ts/index.js';
let api: skr.SkrApi;
tap.test('should initialize API for transaction tests', async () => {
api = new skr.SkrApi({
mongoDbUrl: 'mongodb://localhost:27017',
dbName: 'test_transactions',
});
await api.initialize('SKR03');
expect(api.getSKRType()).toEqual('SKR03');
});
tap.test('should enforce double-entry bookkeeping rules', async () => {
let errorThrown = false;
try {
// Try to post unbalanced journal entry
await api.postJournalEntry({
date: new Date(),
description: 'Unbalanced entry',
reference: 'TEST-001',
lines: [
{ accountNumber: '1000', debit: 100 },
{ accountNumber: '4000', credit: 50 }, // Unbalanced!
],
skrType: 'SKR03',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('not balanced');
}
expect(errorThrown).toBeTrue();
});
tap.test('should prevent posting to same account', async () => {
let errorThrown = false;
try {
await api.postTransaction({
date: new Date(),
debitAccount: '1000',
creditAccount: '1000', // Same account!
amount: 100,
description: 'Invalid transaction',
skrType: 'SKR03',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('cannot be the same');
}
expect(errorThrown).toBeTrue();
});
tap.test('should prevent posting to inactive account', async () => {
// First create and deactivate an account
const customAccount = await api.createAccount({
accountNumber: '9998',
accountName: 'Inactive Test Account',
accountClass: 9,
accountType: 'equity',
isActive: false,
});
let errorThrown = false;
try {
await api.postTransaction({
date: new Date(),
debitAccount: '9998', // Inactive account
creditAccount: '1000',
amount: 100,
description: 'Transaction to inactive account',
skrType: 'SKR03',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('not active');
}
expect(errorThrown).toBeTrue();
});
tap.test(
'should handle complex journal entry with multiple lines',
async () => {
const journalEntry = await api.postJournalEntry({
date: new Date(),
description: 'Complex distribution',
reference: 'COMPLEX-001',
lines: [
{ accountNumber: '5000', debit: 500, description: 'Materials' },
{ accountNumber: '6000', debit: 300, description: 'Wages' },
{ accountNumber: '7100', debit: 200, description: 'Rent' },
{ accountNumber: '1200', credit: 1000, description: 'Bank payment' },
],
skrType: 'SKR03',
});
expect(journalEntry.lines.length).toEqual(4);
expect(journalEntry.totalDebits).toEqual(1000);
expect(journalEntry.totalCredits).toEqual(1000);
expect(journalEntry.isBalanced).toBeTrue();
},
);
tap.test('should track transaction history for account', async () => {
// Post multiple transactions
await api.postTransaction({
date: new Date('2024-01-01'),
debitAccount: '1000',
creditAccount: '4000',
amount: 100,
description: 'Sale 1',
skrType: 'SKR03',
});
await api.postTransaction({
date: new Date('2024-01-02'),
debitAccount: '1000',
creditAccount: '4000',
amount: 200,
description: 'Sale 2',
skrType: 'SKR03',
});
await api.postTransaction({
date: new Date('2024-01-03'),
debitAccount: '5000',
creditAccount: '1000',
amount: 50,
description: 'Purchase',
skrType: 'SKR03',
});
// Get transaction history for cash account
const history = await api.getAccountTransactions('1000');
expect(history.length).toBeGreaterThanOrEqual(3);
// Check balance calculation
const balance = await api.getAccountBalance('1000');
expect(balance.debitTotal).toBeGreaterThanOrEqual(300);
expect(balance.creditTotal).toBeGreaterThanOrEqual(50);
});
tap.test('should filter transactions by date range', async () => {
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-01-31');
const transactions = await api.listTransactions({
dateFrom: startDate,
dateTo: endDate,
});
for (const transaction of transactions) {
expect(transaction.date.getTime()).toBeGreaterThanOrEqual(
startDate.getTime(),
);
expect(transaction.date.getTime()).toBeLessThanOrEqual(endDate.getTime());
}
});
tap.test('should filter transactions by amount range', async () => {
const transactions = await api.listTransactions({
minAmount: 100,
maxAmount: 500,
});
for (const transaction of transactions) {
expect(transaction.amount).toBeGreaterThanOrEqual(100);
expect(transaction.amount).toBeLessThanOrEqual(500);
}
});
tap.test('should handle batch transaction posting', async () => {
const batchTransactions = [
{
date: new Date(),
debitAccount: '1200',
creditAccount: '4000',
amount: 100,
description: 'Batch sale 1',
skrType: 'SKR03' as skr.TSKRType,
},
{
date: new Date(),
debitAccount: '1200',
creditAccount: '4000',
amount: 200,
description: 'Batch sale 2',
skrType: 'SKR03' as skr.TSKRType,
},
{
date: new Date(),
debitAccount: '1200',
creditAccount: '4000',
amount: 300,
description: 'Batch sale 3',
skrType: 'SKR03' as skr.TSKRType,
},
];
const results = await api.postBatchTransactions(batchTransactions);
expect(results.length).toEqual(3);
expect(results[0].amount).toEqual(100);
expect(results[1].amount).toEqual(200);
expect(results[2].amount).toEqual(300);
});
tap.test('should handle transaction with VAT', async () => {
const transaction = await api.postTransaction({
date: new Date(),
debitAccount: '5400', // Goods with 19% VAT
creditAccount: '1600', // Trade payables
amount: 119,
description: 'Purchase including VAT',
skrType: 'SKR03',
vatAmount: 19,
reference: 'VAT-001',
});
expect(transaction.vatAmount).toEqual(19);
expect(transaction.amount).toEqual(119);
});
tap.test('should handle transaction with cost center', async () => {
const transaction = await api.postTransaction({
date: new Date(),
debitAccount: '6000', // Wages
creditAccount: '1200', // Bank
amount: 1000,
description: 'Salary for marketing department',
skrType: 'SKR03',
costCenter: 'MARKETING',
});
expect(transaction.costCenter).toEqual('MARKETING');
});
tap.test('should validate account numbers are 4 digits', async () => {
let errorThrown = false;
try {
await api.createAccount({
accountNumber: '123', // Only 3 digits!
accountName: 'Invalid Account',
accountClass: 1,
accountType: 'asset',
});
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('4 digits');
}
expect(errorThrown).toBeTrue();
});
tap.test('should recalculate all balances', async () => {
await api.recalculateBalances();
// Verify balances are consistent
const trialBalance = await api.generateTrialBalance();
expect(trialBalance.isBalanced).toBeTrue();
});
tap.test('should close API connection', async () => {
await api.close();
});
export default tap.start();