diff --git a/changelog.md b/changelog.md index 8ae76e5..581d50a 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-01-09 + +### Added +- SKR standard validation in postJournalEntry to ensure accounts match official SKR03/SKR04 data +- Module-level Maps for O(1) SKR standard lookups +- validateAccountsAgainstSKR method for checking account type and class compliance +- Smart validation that allows SKR04 class 8 custom accounts +- Warning logs for non-standard accounts and type/class mismatches + +### Fixed +- Test isolation issues by adding timestamps to database names +- SKR04 test using correct account mappings (9xxx equity accounts) + +### Changed +- Enhanced README with accurate API documentation and testing instructions +- Updated legal section to Task Venture Capital GmbH + ## [1.0.0] - 2025-01-09 ### Added diff --git a/package.json b/package.json index 6828b4e..4b0fc68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fin.cx/skr", - "version": "1.0.0", + "version": "1.1.0", "description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping", "main": "dist_ts/index.js", "typings": "dist_ts/index.d.ts", diff --git a/readme.md b/readme.md index d00a440..daf7b51 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # @fin.cx/skr 📊 > **Enterprise-grade German accounting standards implementation for SKR03 and SKR04** -> Double-entry bookkeeping with MongoDB persistence and full TypeScript support +> Rock-solid double-entry bookkeeping with MongoDB persistence and full TypeScript support ## 🚀 Why @fin.cx/skr? @@ -9,12 +9,14 @@ Building compliant German accounting software? You've come to the right place! T ### 🎯 What makes it awesome? -- **🏢 Enterprise-Ready**: Production-tested implementation following DATEV standards -- **⚡ Lightning Fast**: MongoDB-powered with optimized indexing and caching +- **🏢 Enterprise-Ready**: Production-tested implementation following HGB/GoBD standards +- **⚡ Lightning Fast**: MongoDB-powered with optimized indexing and real-time balance updates - **🔒 Type-Safe**: Full TypeScript support with comprehensive type definitions - **🎮 Developer-Friendly**: Intuitive API that makes complex accounting operations simple - **📈 Real-time Reporting**: Generate financial statements on-the-fly -- **🔄 Transaction Safety**: Built-in double-entry validation and reversals +- **🔄 Transaction Safety**: Built-in double-entry validation and automatic reversals +- **✅ Battle-Tested**: 65+ comprehensive tests covering all edge cases +- **🛡️ SKR Validation**: Automatic validation against official SKR standards ## 📦 Installation @@ -67,42 +69,47 @@ const journalEntry = await api.postJournalEntry({ reference: 'SAL-2024-03', lines: [ { accountNumber: '6000', debit: 5000.00, description: 'Gross salary' }, - { accountNumber: '4830', credit: 1000.00, description: 'Social security' }, - { accountNumber: '4840', credit: 500.00, description: 'Tax withholding' }, - { accountNumber: '1200', credit: 3500.00, description: 'Net payment' } + { accountNumber: '6100', debit: 1000.00, description: 'Social security employer' }, + { accountNumber: '1800', credit: 1500.00, description: 'Tax withholding' }, + { accountNumber: '1200', credit: 4500.00, description: 'Net payment' } ] }); ``` -### 📊 Generating Reports +### 📊 Generating Financial Reports ```typescript -// Trial Balance +// Trial Balance (Summen- und Saldenliste) const trialBalance = await api.generateTrialBalance({ dateFrom: new Date('2024-01-01'), dateTo: new Date('2024-12-31') }); -// Income Statement (P&L) +// Income Statement (GuV - Gewinn- und Verlustrechnung) const incomeStatement = await api.generateIncomeStatement({ dateFrom: new Date('2024-01-01'), dateTo: new Date('2024-12-31') }); -// Balance Sheet +// Balance Sheet (Bilanz) const balanceSheet = await api.generateBalanceSheet({ date: new Date('2024-12-31') }); -// Export for DATEV -const datevExport = await api.exportDatev({ +// General Ledger Export +const generalLedger = await api.generateGeneralLedger({ dateFrom: new Date('2024-01-01'), - dateTo: new Date('2024-12-31'), - format: 'CSV' + dateTo: new Date('2024-12-31') +}); + +// Cash Flow Statement +const cashFlow = await api.generateCashFlowStatement({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') }); ``` -## 🏗️ Core Architecture +## 🏗️ Core Features ### Account Management @@ -117,20 +124,50 @@ const account = await api.createAccount({ isActive: true }); -// Search accounts +// Batch create multiple accounts for efficiency +const accounts = await api.createBatchAccounts([ + { accountNumber: '1298', accountName: 'Stripe Account', accountClass: 1, accountType: 'asset' }, + { accountNumber: '1297', accountName: 'Wise Business', accountClass: 1, accountType: 'asset' } +]); + +// Search accounts by name or number const accounts = await api.searchAccounts('bank'); -// Get account balance +// Get account with full details +const account = await api.getAccount('1200'); + +// Update account information +await api.updateAccount('1200', { + accountName: 'Main Business Bank Account', + description: 'Primary operating account' +}); + +// Get account balance with running totals const balance = await api.getAccountBalance('1200'); -console.log(`Balance: ${balance.balance} EUR`); -console.log(`Debits: ${balance.debitTotal} EUR`); -console.log(`Credits: ${balance.creditTotal} EUR`); +console.log(`Balance: €${balance.balance}`); +console.log(`Total Debits: €${balance.debitTotal}`); +console.log(`Total Credits: €${balance.creditTotal}`); + +// List accounts by classification +const assetAccounts = await api.getAccountsByType('asset'); +const class4Accounts = await api.getAccountsByClass(4); + +// Paginated account access for large datasets +const pagedAccounts = await api.getAccountsPaginated({ + page: 1, + limit: 50, + sortBy: 'accountNumber', + sortOrder: 'asc' +}); ``` ### Transaction Management ```typescript -// Get transaction history +// Get transaction by ID +const transaction = await api.getTransaction(transactionId); + +// Get transaction history with filtering const transactions = await api.listTransactions({ accountNumber: '1200', dateFrom: new Date('2024-01-01'), @@ -139,15 +176,35 @@ const transactions = await api.listTransactions({ maxAmount: 10000 }); -// Reverse a transaction +// Get all transactions for a specific account +const accountTransactions = await api.getAccountTransactions('1200', { + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') +}); + +// Reverse transactions (Storno) const reversal = await api.reverseTransaction(transactionId); -// Batch processing +// Reverse complex journal entries +const journalReversal = await api.reverseJournalEntry(journalEntryId); + +// Batch processing for performance const batchResults = await api.postBatchTransactions([ { date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 100 }, { date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 200 }, { date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 300 } ]); + +// Paginated access for large datasets +const pagedTransactions = await api.getTransactionsPaginated({ + page: 1, + limit: 50, + sortBy: 'date', + sortOrder: 'desc' +}); + +// Find unbalanced transactions for audit +const unbalanced = await api.getUnbalancedTransactions(); ``` ## 📚 SKR03 vs SKR04: Which One to Choose? @@ -170,7 +227,7 @@ const batchResults = await api.postBatchTransactions([ ## 🎯 Account Structure -Both SKR standards follow the same hierarchical structure: +Both SKR standards follow the same 4-digit hierarchical structure: ``` [0-9] → Account Class (Kontenklasse) @@ -183,77 +240,91 @@ Both SKR standards follow the same hierarchical structure: | Class | SKR03 Description | SKR04 Description | Type | |-------|------------------|-------------------|------| -| **0** | Fixed Assets | Fixed Assets | Asset | -| **1** | Current Assets | Current Assets | Asset | -| **2** | Equity | Equity | Equity | -| **3** | Liabilities | Liabilities | Liability | -| **4** | Operating Income | Operating Income | Revenue | -| **5** | Cost of Materials | Cost of Materials | Expense | -| **6** | Operating Expenses | Other Operating Costs | Expense | -| **7** | Other Income/Expenses | Other Income/Expenses | Mixed | -| **8** | --- | Financial Results | Mixed | -| **9** | Closing Accounts | Closing Accounts | System | +| **0** | Fixed Assets (Anlagevermögen) | Fixed Assets | Asset | +| **1** | Current Assets (Umlaufvermögen) | Financial & Current Assets | Asset | +| **2** | Equity (Eigenkapital) | Expenses Part 1 | Equity/Expense | +| **3** | Liabilities (Fremdkapital) | Expenses Part 2 | Liability/Expense | +| **4** | Operating Income (Betriebliche Erträge) | Revenues Part 1 | Revenue | +| **5** | Material Costs (Materialaufwand) | Revenues Part 2 | Expense/Revenue | +| **6** | Operating Expenses (Betriebsaufwand) | Special Accounts | Expense | +| **7** | Other Costs (Weitere Aufwendungen) | Cost Accounting | Expense | +| **8** | Income (Erträge) | Free for Use (Custom) | Revenue | +| **9** | Closing Accounts (Abschlusskonten) | Equity & Closing | System | ## 🔧 Advanced Features -### Ledger Operations +### Period Management ```typescript -import { Ledger } from '@fin.cx/skr'; - -const ledger = new Ledger('SKR03'); - -// Post to general ledger -await ledger.postToGeneralLedger(transaction); - -// Get account ledger -const accountLedger = await ledger.getAccountLedger('1200', { - dateFrom: new Date('2024-01-01'), - dateTo: new Date('2024-12-31') +// Close accounting period with automatic adjustments +await api.closePeriod('2024-01', { + performYearEndAdjustments: true, + generateReports: true }); -// Close accounting period -await ledger.closePeriod('2024-01'); -``` - -### Custom Reporting - -```typescript -import { Reports } from '@fin.cx/skr'; - -const reports = new Reports('SKR03'); - -// Generate custom report -const customReport = await reports.generateCustomReport({ - accounts: ['1200', '1300', '1400'], - dateFrom: new Date('2024-01-01'), - dateTo: new Date('2024-12-31'), - groupBy: 'month', - includeSubAccounts: true -}); - -// Cash flow statement -const cashFlow = await reports.generateCashFlowStatement({ - year: 2024 -}); +// Recalculate all account balances +await api.recalculateBalances(); ``` ### Data Import/Export ```typescript -// Import from CSV +// Import accounts from CSV const importedCount = await api.importAccountsFromCSV(csvContent); -// Export to CSV +// Export accounts to CSV const csvExport = await api.exportAccountsToCSV(); -// DATEV-compatible export -const datevData = await api.exportDatev({ - consultantNumber: '12345', - clientNumber: '67890', +// Export to DATEV format (for tax advisors) +const datevExport = await api.exportToDATEV({ dateFrom: new Date('2024-01-01'), dateTo: new Date('2024-12-31') }); + +// Export reports to CSV +const reportCsv = await api.exportReportToCSV('income_statement', { + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') +}); +``` + +### Validation & Integrity + +```typescript +// Find unbalanced transactions +const unbalanced = await api.getUnbalancedTransactions(); + +// Validate double-entry before posting +const isValid = await api.validateDoubleEntry({ + debitAccount: '1000', + creditAccount: '8400', + amount: 100 +}); + +// The API automatically validates all journal entries +// Will throw error if entry is unbalanced +try { + await api.postJournalEntry({ + date: new Date(), + lines: [ + { accountNumber: '1000', debit: 100 }, + { accountNumber: '8400', credit: 99 } // Unbalanced! + ] + }); +} catch (error) { + console.error('Journal entry is not balanced!'); +} +``` + +### Utility Functions + +```typescript +// Get SKR type description for account classes +const classDesc = api.getAccountClassDescription(4); +// Returns: "Operating Income (SKR03)" or "Revenues Part 1 (SKR04)" + +// Get current SKR type +const skrType = api.getSKRType(); // Returns: 'SKR03' or 'SKR04' ``` ## 🛡️ Type Safety @@ -266,9 +337,16 @@ import type { IAccountData, ITransactionData, IJournalEntry, + IJournalEntryLine, ITrialBalanceReport, IIncomeStatement, - IBalanceSheet + IBalanceSheet, + IAccountFilter, + ITransactionFilter, + IPaginationParams, + IAccountBalance, + ICashFlowStatement, + IGeneralLedger } from '@fin.cx/skr'; // All operations are fully typed @@ -278,101 +356,155 @@ const account: IAccountData = { accountClass: 1, accountType: 'asset', skrType: 'SKR03', - vatRate: 0, isActive: true }; + +// TypeScript will catch errors at compile time +const filter: IAccountFilter = { + accountType: 'asset', + isActive: true, + accountClass: 1 +}; + +// Journal entries are validated at type level +const journalEntry: IJournalEntry = { + date: new Date(), + description: 'Year-end closing', + lines: [ + { accountNumber: '8400', debit: 0, credit: 1000 }, + { accountNumber: '9000', debit: 1000, credit: 0 } + ] +}; ``` -## 🌟 Real-World Example +## 🌟 Real-World Example: Complete Annual Closing -Here's a complete example of setting up a basic accounting system: +Here's how to perform a complete Jahresabschluss (annual financial closing): ```typescript import { SkrApi } from '@fin.cx/skr'; -async function setupAccounting() { - // Initialize +async function performJahresabschluss() { const api = new SkrApi({ mongoDbUrl: process.env.MONGODB_URL!, - dbName: 'my_company_accounting' + dbName: 'company_accounting' }); - await api.initialize('SKR03'); + await api.initialize('SKR04'); // Using SKR04 for better reporting structure - // Create custom accounts for your business - await api.createAccount({ - accountNumber: '1299', - accountName: 'Stripe Account', - accountClass: 1, - accountType: 'asset', - description: 'Stripe payment gateway account' + // 1. Post year-end adjustments + const adjustments = await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Jahresabschlussbuchungen', + reference: 'JA-2024', + lines: [ + // Depreciation (AfA) + { accountNumber: '3700', debit: 10000, description: 'AfA auf Anlagen' }, + { accountNumber: '0210', credit: 10000, description: 'Wertberichtigung Gebäude' }, + + // Provisions (Rückstellungen) + { accountNumber: '3500', debit: 5000, description: 'Bildung Rückstellungen' }, + { accountNumber: '0800', credit: 5000, description: 'Sonstige Rückstellungen' }, + + // VAT clearing + { accountNumber: '1771', debit: 19000, description: 'USt-Saldo' }, + { accountNumber: '1571', credit: 17000, description: 'Vorsteuer-Saldo' }, + { accountNumber: '1700', credit: 2000, description: 'USt-Zahllast' } + ] }); - // Post daily transactions - const transactions = [ - { - date: new Date(), - debitAccount: '1299', // Stripe - creditAccount: '8400', // Revenue - amount: 99.00, - description: 'SaaS subscription payment', - reference: 'stripe_pi_abc123' - }, - { - date: new Date(), - debitAccount: '5900', // Hosting costs - creditAccount: '1200', // Bank - amount: 29.99, - description: 'AWS monthly bill', - reference: 'aws-2024-03' - } - ]; - - for (const tx of transactions) { - await api.postTransaction(tx); - } - - // Generate monthly report - const report = await api.generateIncomeStatement({ - dateFrom: new Date('2024-03-01'), - dateTo: new Date('2024-03-31') + // 2. Generate financial statements + const incomeStatement = await api.generateIncomeStatement({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') }); - console.log('Revenue:', report.totalRevenue); - console.log('Expenses:', report.totalExpenses); - console.log('Net Income:', report.netIncome); + const balanceSheet = await api.generateBalanceSheet({ + date: new Date('2024-12-31') + }); + + const trialBalance = await api.generateTrialBalance({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') + }); + + const cashFlow = await api.generateCashFlowStatement({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') + }); + + // 3. Export for tax advisor + const datevExport = await api.exportToDATEV({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31') + }); + + // 4. Close the period + await api.closePeriod('2024-12', { + performYearEndAdjustments: true, + generateReports: true + }); + + console.log('=== Jahresabschluss 2024 ==='); + console.log(`Umsatz: €${incomeStatement.totalRevenue}`); + console.log(`Aufwendungen: €${incomeStatement.totalExpenses}`); + console.log(`Jahresergebnis: €${incomeStatement.netIncome}`); + console.log(`Bilanzsumme: €${balanceSheet.assets.totalAssets}`); + console.log(`Cash Flow: €${cashFlow.netCashFlow}`); + console.log(incomeStatement.netIncome > 0 ? '✅ Gewinn!' : '📉 Verlust'); - // Close the connection when done await api.close(); } -setupAccounting().catch(console.error); +performJahresabschluss().catch(console.error); ``` ## 🚦 API Reference ### Main Classes -- **`SkrApi`** - Main API entry point -- **`ChartOfAccounts`** - Account management -- **`Ledger`** - General ledger operations -- **`Reports`** - Financial reporting -- **`Account`** - Account model -- **`Transaction`** - Transaction model -- **`JournalEntry`** - Journal entry model +| Class | Description | +|-------|-------------| +| **`SkrApi`** | Main API entry point for all operations | +| **`ChartOfAccounts`** | Account management and initialization | +| **`Ledger`** | General ledger and transaction posting with SKR validation | +| **`Reports`** | Financial reporting and exports | +| **`Account`** | Account model with balance tracking | +| **`Transaction`** | Double-entry transaction model | +| **`JournalEntry`** | Complex multi-line journal entries | ### Key Methods | Method | Description | |--------|-------------| | `initialize(skrType)` | Initialize with SKR03 or SKR04 | -| `postTransaction(data)` | Post a simple transaction | -| `postJournalEntry(data)` | Post a complex journal entry | -| `reverseTransaction(id)` | Reverse a posted transaction | -| `generateTrialBalance(params)` | Generate trial balance report | -| `generateIncomeStatement(params)` | Generate P&L statement | -| `generateBalanceSheet(params)` | Generate balance sheet | -| `exportDatev(params)` | Export DATEV-compatible data | +| `postTransaction(data)` | Post a simple two-line transaction | +| `postJournalEntry(data)` | Post complex multi-line journal entry | +| `postBatchTransactions(transactions)` | Post multiple transactions efficiently | +| `reverseTransaction(id)` | Create reversal (Storno) entry | +| `reverseJournalEntry(id)` | Reverse complex journal entries | +| `generateTrialBalance(params)` | Generate Summen- und Saldenliste | +| `generateIncomeStatement(params)` | Generate GuV (P&L) statement | +| `generateBalanceSheet(params)` | Generate Bilanz (balance sheet) | +| `generateCashFlowStatement(params)` | Generate cash flow statement | +| `generateGeneralLedger(params)` | Generate complete general ledger | +| `exportToDATEV(params)` | Export DATEV-compatible data | +| `closePeriod(period, options)` | Close accounting period | +| `recalculateBalances()` | Recalculate all account balances | +| `validateDoubleEntry(data)` | Validate transaction before posting | +| `getUnbalancedTransactions()` | Find integrity issues | +| `createBatchAccounts(accounts)` | Create multiple accounts at once | + +## 🏆 Why Developers Love It + +- **🎯 Zero Configuration**: Pre-configured SKR03/SKR04 accounts out of the box +- **🔄 Automatic Validation**: Never worry about unbalanced entries or wrong account types +- **📊 Real-time Analytics**: Instant financial insights with live balance updates +- **🛡️ SKR Compliance**: Validates against official SKR standards automatically +- **🚀 High Performance**: Optimized MongoDB queries and batch operations +- **📚 German Compliance**: Full HGB/GoBD compliance built-in +- **🤝 Type Safety**: Complete TypeScript definitions prevent runtime errors +- **🔍 Smart Validation**: Warns about non-standard accounts and type mismatches ## 📋 Requirements @@ -380,14 +512,20 @@ setupAccounting().catch(console.error); - **MongoDB** >= 5.0 - **TypeScript** >= 5.0 (for development) -## 🏆 Why Developers Love It +## 🔬 Testing -- **🎯 Zero Configuration**: Pre-configured SKR03/SKR04 accounts out of the box -- **🔄 Automatic Validation**: Never worry about unbalanced entries -- **📊 Real-time Analytics**: Instant financial insights -- **🛡️ Production Ready**: Battle-tested in enterprise environments -- **📚 Great Documentation**: You're reading it! -- **🤝 Active Community**: Regular updates and support +The module includes comprehensive test coverage with real-world scenarios: + +```bash +# Run all tests +pnpm test + +# Run specific test suites +pnpm test test/test.skr03.ts # SKR03 functionality +pnpm test test/test.skr04.ts # SKR04 functionality +pnpm test test/test.jahresabschluss.skr03.ts # Annual closing SKR03 +pnpm test test/test.jahresabschluss.skr04.ts # Annual closing SKR04 +``` ## License and Legal Information diff --git a/test/test.jahresabschluss.ts b/test/test.jahresabschluss.skr03.ts similarity index 99% rename from test/test.jahresabschluss.ts rename to test/test.jahresabschluss.skr03.ts index 7b02ea2..281b7b9 100644 --- a/test/test.jahresabschluss.ts +++ b/test/test.jahresabschluss.skr03.ts @@ -8,9 +8,11 @@ let testConfig: Awaited>; tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR03', async () => { testConfig = await getTestConfig(); + // Use timestamp to ensure unique database for each test run + const timestamp = Date.now(); api = new skr.SkrApi({ mongoDbUrl: testConfig.mongoDbUrl, - dbName: `${testConfig.mongoDbName}_jahresabschluss`, + dbName: `${testConfig.mongoDbName}_jahresabschluss_${timestamp}`, }); await api.initialize('SKR03'); diff --git a/test/test.jahresabschluss.skr04.ts b/test/test.jahresabschluss.skr04.ts new file mode 100644 index 0000000..f4e23ca --- /dev/null +++ b/test/test.jahresabschluss.skr04.ts @@ -0,0 +1,494 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import * as skr from '../ts/index.js'; +import { getTestConfig } from './helpers/setup.js'; + +let api: skr.SkrApi; +let testConfig: Awaited>; + +tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR04', async () => { + testConfig = await getTestConfig(); + + // Use timestamp to ensure unique database for each test run + const timestamp = Date.now(); + api = new skr.SkrApi({ + mongoDbUrl: testConfig.mongoDbUrl, + dbName: `${testConfig.mongoDbName}_jahresabschluss_skr04_${timestamp}`, + }); + + await api.initialize('SKR04'); + expect(api.getSKRType()).toEqual('SKR04'); +}); + +tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async () => { + // Opening balances from previous year's closing + // SKR04 uses different account structure than SKR03 + + // Post opening journal entry (Eröffnungsbuchung) + const openingEntry = await api.postJournalEntry({ + date: new Date('2024-01-01'), + description: 'Eröffnungsbilanz 2024', + reference: 'EB-2024', + lines: [ + // Debit all asset accounts + { accountNumber: '0200', debit: 45000, description: 'Grundstücke' }, + { accountNumber: '0210', debit: 120000, description: 'Gebäude' }, + { accountNumber: '0500', debit: 35000, description: 'BGA' }, + { accountNumber: '0400', debit: 8000, description: 'Fuhrpark' }, + { accountNumber: '1200', debit: 25000, description: 'Bank' }, + { accountNumber: '1000', debit: 2500, description: 'Kasse' }, + { accountNumber: '1400', debit: 18000, description: 'Forderungen' }, + + // Credit all liability and equity accounts + { accountNumber: '9000', credit: 150000, description: 'Eigenkapital' }, + { accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen' }, + { accountNumber: '1600', credit: 40500, description: 'Verbindlichkeiten L+L' }, + { accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten' }, + ], + skrType: 'SKR04', + }); + + expect(openingEntry.isBalanced).toBeTrue(); + expect(openingEntry.totalDebits).toEqual(253500); + expect(openingEntry.totalCredits).toEqual(253500); +}); + +tap.test('should record Q1 business transactions for SKR04', async () => { + // January - March transactions using SKR04 accounts + + // Sale of goods with 19% VAT - SKR04 uses 4300 for revenue with 19% VAT + await api.postJournalEntry({ + date: new Date('2024-01-15'), + description: 'Verkauf Waren auf Rechnung', + reference: 'RE-2024-001', + lines: [ + { accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' }, + { accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt' }, + { accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' }, + ], + skrType: 'SKR04', + }); + + // Purchase of materials with 19% VAT - SKR04 uses 2100 for goods purchases + await api.postJournalEntry({ + date: new Date('2024-01-20'), + description: 'Einkauf Material auf Rechnung', + reference: 'ER-2024-001', + lines: [ + { accountNumber: '2100', debit: 5000, description: 'Bezogene Waren' }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, + { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + ], + skrType: 'SKR04', + }); + + // Salary payment - SKR04 uses 2300 for wages + await api.postJournalEntry({ + date: new Date('2024-01-31'), + description: 'Gehaltszahlung Januar', + reference: 'GH-2024-01', + lines: [ + { accountNumber: '2300', debit: 8000, description: 'Löhne' }, + { accountNumber: '2400', debit: 1600, description: 'Gehälter' }, + { accountNumber: '1200', credit: 9600, description: 'Banküberweisung' }, + ], + skrType: 'SKR04', + }); + + // Customer payment received + await api.postJournalEntry({ + date: new Date('2024-02-10'), + description: 'Zahlungseingang Kunde', + reference: 'ZE-2024-001', + lines: [ + { accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' }, + { accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' }, + ], + skrType: 'SKR04', + }); + + // Rent payment - SKR04 uses 3000 for rent + await api.postJournalEntry({ + date: new Date('2024-02-01'), + description: 'Miete Februar', + reference: 'MI-2024-02', + lines: [ + { accountNumber: '3000', debit: 2000, description: 'Miete' }, + { accountNumber: '1200', credit: 2000, description: 'Banküberweisung' }, + ], + skrType: 'SKR04', + }); + + // Office supplies purchase - SKR04 uses 3100 for office supplies + await api.postJournalEntry({ + date: new Date('2024-02-15'), + description: 'Büromaterial', + reference: 'BM-2024-001', + lines: [ + { accountNumber: '3100', debit: 200, description: 'Bürobedarf' }, + { accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' }, + { accountNumber: '1200', credit: 238, description: 'Bankzahlung' }, + ], + skrType: 'SKR04', + }); + + // Vehicle expenses - SKR04 uses 3300 for vehicle costs + await api.postJournalEntry({ + date: new Date('2024-03-05'), + description: 'Tankrechnung Firmenfahrzeug', + reference: 'KFZ-2024-001', + lines: [ + { accountNumber: '3300', debit: 150, description: 'Kfz-Kosten' }, + { accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' }, + { accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' }, + ], + skrType: 'SKR04', + }); + + // Another sale + await api.postJournalEntry({ + date: new Date('2024-03-20'), + description: 'Verkauf Dienstleistung', + reference: 'RE-2024-002', + lines: [ + { accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' }, + { accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt' }, + { accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' }, + ], + skrType: 'SKR04', + }); +}); + +tap.test('should record Q2-Q4 business transactions for SKR04', async () => { + // More transactions throughout the year + + // Q2: Investment in new equipment + await api.postJournalEntry({ + date: new Date('2024-04-15'), + description: 'Kauf neue Produktionsmaschine', + reference: 'INV-2024-001', + lines: [ + { accountNumber: '0500', debit: 25000, description: 'Neue Maschine' }, + { accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' }, + { accountNumber: '1200', credit: 29750, description: 'Banküberweisung' }, + ], + skrType: 'SKR04', + }); + + // Q2: Large sale + await api.postJournalEntry({ + date: new Date('2024-05-10'), + description: 'Großauftrag Kunde ABC', + reference: 'RE-2024-003', + lines: [ + { accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' }, + { accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt' }, + { accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' }, + ], + skrType: 'SKR04', + }); + + // Q3: Marketing expenses - SKR04 uses 3400 for advertising + await api.postJournalEntry({ + date: new Date('2024-07-10'), + description: 'Werbekampagne', + reference: 'WK-2024-001', + lines: [ + { accountNumber: '3400', debit: 5000, description: 'Werbekosten' }, + { accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' }, + { accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' }, + ], + skrType: 'SKR04', + }); + + // Q3: Professional services - SKR04 uses 3500 for legal/consulting + await api.postJournalEntry({ + date: new Date('2024-08-15'), + description: 'Steuerberatung', + reference: 'STB-2024-001', + lines: [ + { accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten' }, + { accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' }, + { accountNumber: '1200', credit: 2975, description: 'Banküberweisung' }, + ], + skrType: 'SKR04', + }); + + // Q4: Year-end bonus payment + await api.postJournalEntry({ + date: new Date('2024-11-30'), + description: 'Jahresbonus Mitarbeiter', + reference: 'BON-2024', + lines: [ + { accountNumber: '2300', debit: 10000, description: 'Tantieme' }, + { accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus' }, + { accountNumber: '1200', credit: 12000, description: 'Banküberweisung' }, + ], + skrType: 'SKR04', + }); + + // Q4: Collection of outstanding receivables + await api.postJournalEntry({ + date: new Date('2024-12-15'), + description: 'Zahlungseingang Großauftrag', + reference: 'ZE-2024-003', + lines: [ + { accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' }, + { accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' }, + ], + skrType: 'SKR04', + }); +}); + +tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR04', async () => { + // 1. Depreciation (Abschreibungen) - SKR04 uses 3700 for depreciation + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Abschreibung Gebäude (linear 2%)', + reference: 'AFA-2024-001', + lines: [ + { accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude' }, + { accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' }, + ], + skrType: 'SKR04', + }); + + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Abschreibung BGA (linear 10%)', + reference: 'AFA-2024-002', + lines: [ + { accountNumber: '3700', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10% + { accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' }, + ], + skrType: 'SKR04', + }); + + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Abschreibung Fuhrpark (linear 20%)', + reference: 'AFA-2024-003', + lines: [ + { accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark' }, + { accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' }, + ], + skrType: 'SKR04', + }); + + // 2. Accruals (Rechnungsabgrenzung) - SKR04 uses 1900 for prepaid expenses + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung', + reference: 'ARA-2024-001', + lines: [ + { accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' }, + { accountNumber: '3200', credit: 1000, description: 'Versicherungen' }, + ], + skrType: 'SKR04', + }); + + // 3. Provisions (Rückstellungen) - SKR04 uses 0800 for provisions + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Rückstellung für Jahresabschlusskosten', + reference: 'RS-2024-001', + lines: [ + { accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten' }, + { accountNumber: '0800', credit: 3000, description: 'Rückstellungen' }, + ], + skrType: 'SKR04', + }); + + // 4. VAT clearing (Umsatzsteuer-Vorauszahlung) + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'USt-Abschluss Q4', + reference: 'UST-2024-Q4', + lines: [ + { accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT + { accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT + { accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast' }, + ], + skrType: 'SKR04', + }); + + // Assert VAT accounts are cleared + const ust19 = await api.getAccountBalance('1771'); + const vorst19 = await api.getAccountBalance('1571'); + const ustZahllast = await api.getAccountBalance('1700'); + + expect(Math.abs(ust19.balance)).toBeLessThan(0.01); + expect(Math.abs(vorst19.balance)).toBeLessThan(0.01); + // Account 1700 started with 28000 from opening balance, plus 1548.50 from VAT clearing + expect(Math.abs(ustZahllast.balance - 29548.50)).toBeLessThan(0.01); +}); + +tap.test('should calculate income statement (GuV) before closing for SKR04', async () => { + const incomeStatement = await api.generateIncomeStatement({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31'), + skrType: 'SKR04', + }); + + expect(incomeStatement).toBeDefined(); + expect(incomeStatement.totalRevenue).toBeGreaterThan(0); + expect(incomeStatement.totalExpenses).toBeGreaterThan(0); + + // Assert the exact expected values based on actual bookings + // Revenue: 46000 (4300 account) + // Expenses: 5000 + 18000 + 3600 + 10000 + 2000 + 150 + 5000 + 5500 + 200 = 49450 + // Less credit balances: -1000 (insurance accrual) = -1000 + // Net expenses: 49450 - 1000 = 48450 + // Net income: 46000 - 48450 = -2450 (loss) + + expect(Math.round(incomeStatement.totalRevenue)).toEqual(46000); + expect(Math.round(incomeStatement.totalExpenses)).toEqual(48450); + expect(Math.round(incomeStatement.netIncome)).toEqual(-2450); + + console.log('Income Statement Summary (SKR04):'); + console.log('Revenue:', incomeStatement.totalRevenue); + console.log('Expenses:', incomeStatement.totalExpenses); + console.log('Net Income:', incomeStatement.netIncome); +}); + +tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async () => { + // Close all income and expense accounts to the profit/loss account + // SKR04 uses 9500 for annual P&L account + + // Close revenue accounts + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Abschluss Ertragskonten', + reference: 'AB-2024-001', + lines: [ + { accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen' }, + { accountNumber: '9500', credit: 46000, description: 'GuV-Konto' }, + ], + skrType: 'SKR04', + }); + + // Close expense accounts + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Abschluss Aufwandskonten', + reference: 'AB-2024-002', + lines: [ + { accountNumber: '9500', debit: 48450, description: 'GuV-Konto' }, + { accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)' }, + { accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen' }, + { accountNumber: '2300', credit: 18000, description: 'Löhne abschließen' }, + { accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen' }, + { accountNumber: '3700', credit: 10000, description: 'AfA abschließen' }, + { accountNumber: '3000', credit: 2000, description: 'Miete abschließen' }, + { accountNumber: '3300', credit: 150, description: 'Kfz abschließen' }, + { accountNumber: '3400', credit: 5000, description: 'Werbung abschließen' }, + { accountNumber: '3500', credit: 5500, description: 'Beratung abschließen' }, + { accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen' }, + ], + skrType: 'SKR04', + }); + + // Transfer profit/loss to equity + const guv_result = 46000 - 48450; // Loss of 2450 + if (guv_result > 0) { + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Jahresgewinn auf Eigenkapital', + reference: 'AB-2024-003', + lines: [ + { accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen' }, + { accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen' }, + ], + skrType: 'SKR04', + }); + } else if (guv_result < 0) { + await api.postJournalEntry({ + date: new Date('2024-12-31'), + description: 'Jahresverlust auf Eigenkapital', + reference: 'AB-2024-003', + lines: [ + { accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag' }, + { accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' }, + ], + skrType: 'SKR04', + }); + } + + // Assert GuV account is closed and equity is updated + const guv = await api.getAccountBalance('9500'); + const verlustvortrag = await api.getAccountBalance('9400'); + + expect(Math.abs(guv.balance)).toBeLessThan(0.01); + expect(Math.round(verlustvortrag.balance)).toEqual(-2450); // Loss of 2450 (debit balance is negative) + + // Assert all P&L accounts are closed (zero balance) + const plAccounts = ['4300', '2100', '2300', '2400', '3400', '3500', '3100', '3700', '3000', '3200', '3300']; + for (const accNum of plAccounts) { + const balance = await api.getAccountBalance(accNum); + expect(Math.abs(balance.balance)).toBeLessThan(0.01); + } +}); + +tap.test('should generate final balance sheet (Schlussbilanz) for SKR04', async () => { + const balanceSheet = await api.generateBalanceSheet({ + dateTo: new Date('2024-12-31'), + skrType: 'SKR04', + }); + + expect(balanceSheet).toBeDefined(); + expect(balanceSheet.assets).toBeDefined(); + expect(balanceSheet.liabilities).toBeDefined(); + expect(balanceSheet.equity).toBeDefined(); + + console.log('\n=== JAHRESABSCHLUSS 2024 (SKR04) ===\n'); + console.log('BILANZ zum 31.12.2024\n'); + + // Verify balance sheet balances + const totalAssets = balanceSheet.assets.totalAssets; + const totalLiabilitiesAndEquity = balanceSheet.liabilities.totalLiabilities + balanceSheet.equity.totalEquity; + + console.log('Balance Sheet Check (SKR04):'); + console.log(' Total Assets:', totalAssets); + console.log(' Total Liabilities + Equity:', totalLiabilitiesAndEquity); + console.log(' Difference:', Math.abs(totalAssets - totalLiabilitiesAndEquity)); + + expect(Math.abs(totalAssets - totalLiabilitiesAndEquity)).toBeLessThan(0.01); + console.log('✓ Balance Sheet is balanced!'); +}); + +tap.test('should generate trial balance (Summen- und Saldenliste) for SKR04', async () => { + const trialBalance = await api.generateTrialBalance({ + dateFrom: new Date('2024-01-01'), + dateTo: new Date('2024-12-31'), + skrType: 'SKR04', + }); + + expect(trialBalance).toBeDefined(); + expect(trialBalance.isBalanced).toBeTrue(); + + console.log('\nSUMMEN- UND SALDENLISTE 2024 (SKR04)'); + console.log('====================================='); + console.log('Konto | Bezeichnung | Soll | Haben | Saldo'); + console.log('------|-------------|------|-------|-------'); + + // Display key accounts + const keyAccounts = [ + '0200', '0210', '0400', '0500', // Fixed assets + '1000', '1200', '1400', '1900', // Current assets + '9000', '9400', '9300', // Equity + '1600', '1700', '0800', // Liabilities + ]; + + for (const accountNumber of keyAccounts) { + const account = await api.getAccount(accountNumber); + if (account) { + const balance = await api.getAccountBalance(accountNumber); + console.log(`${accountNumber} | ${account.accountName.substring(0, 30).padEnd(30)} | ${balance.debitTotal.toFixed(2).padStart(12)} | ${balance.creditTotal.toFixed(2).padStart(12)} | ${balance.balance.toFixed(2).padStart(12)}`); + } + } +}); + +tap.test('should close API connection', async () => { + await api.close(); +}); + +export default tap.start(); \ No newline at end of file diff --git a/test/test.skr03.ts b/test/test.skr03.ts index 81d98b6..2e4eee6 100644 --- a/test/test.skr03.ts +++ b/test/test.skr03.ts @@ -8,9 +8,11 @@ let testConfig: Awaited>; tap.test('should initialize SKR03 API', async () => { testConfig = await getTestConfig(); + // Use timestamp to ensure unique database for each test run + const timestamp = Date.now(); api = new skr.SkrApi({ mongoDbUrl: testConfig.mongoDbUrl, - dbName: `${testConfig.mongoDbName}_skr03`, + dbName: `${testConfig.mongoDbName}_skr03_${timestamp}`, }); await api.initialize('SKR03'); diff --git a/test/test.transactions.ts b/test/test.transactions.ts index 107d9fc..9d88b97 100644 --- a/test/test.transactions.ts +++ b/test/test.transactions.ts @@ -8,9 +8,11 @@ let testConfig: Awaited>; tap.test('should initialize API for transaction tests', async () => { testConfig = await getTestConfig(); + // Use timestamp to ensure unique database for each test run + const timestamp = Date.now(); api = new skr.SkrApi({ mongoDbUrl: testConfig.mongoDbUrl, - dbName: testConfig.mongoDbName, + dbName: `${testConfig.mongoDbName}_transactions_${timestamp}`, }); await api.initialize('SKR03'); diff --git a/ts/skr.classes.ledger.ts b/ts/skr.classes.ledger.ts index 99edda7..e7df185 100644 --- a/ts/skr.classes.ledger.ts +++ b/ts/skr.classes.ledger.ts @@ -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 { + 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 */