feat(tests): integrate qenv for dynamic configuration and enhance SKR API tests
This commit is contained in:
@@ -33,7 +33,8 @@
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.2"
|
||||
"@git.zone/tstest": "^2.3.2",
|
||||
"@push.rocks/qenv": "^6.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -30,6 +30,9 @@ importers:
|
||||
'@git.zone/tstest':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.6)(typescript@5.8.3)
|
||||
'@push.rocks/qenv':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
|
||||
packages:
|
||||
|
||||
|
41
test/helpers/setup.ts
Normal file
41
test/helpers/setup.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
|
||||
// Initialize qenv to load environment variables from .nogit folder
|
||||
const testQenv = new qenv.Qenv('./', './.nogit/');
|
||||
|
||||
// Export configuration for MongoDB and S3
|
||||
export const getTestConfig = async () => {
|
||||
// Try to get individual MongoDB components first
|
||||
const mongoHost = await testQenv.getEnvVarOnDemand('MONGODB_HOST') || 'localhost';
|
||||
const mongoPort = await testQenv.getEnvVarOnDemand('MONGODB_PORT') || '27017';
|
||||
const mongoUser = await testQenv.getEnvVarOnDemand('MONGODB_USER');
|
||||
const mongoPass = await testQenv.getEnvVarOnDemand('MONGODB_PASS');
|
||||
const mongoDbName = await testQenv.getEnvVarOnDemand('MONGODB_NAME') || 'test_skr';
|
||||
|
||||
// Build MongoDB URL with authentication
|
||||
let mongoDbUrl: string;
|
||||
if (mongoUser && mongoPass) {
|
||||
// Include authSource=admin for authentication
|
||||
mongoDbUrl = `mongodb://${mongoUser}:${mongoPass}@${mongoHost}:${mongoPort}/?authSource=admin`;
|
||||
} else {
|
||||
mongoDbUrl = `mongodb://${mongoHost}:${mongoPort}`;
|
||||
}
|
||||
|
||||
// Get S3 configuration
|
||||
const s3Host = await testQenv.getEnvVarOnDemand('S3_HOST');
|
||||
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT');
|
||||
const s3User = await testQenv.getEnvVarOnDemand('S3_USER');
|
||||
const s3Pass = await testQenv.getEnvVarOnDemand('S3_PASS');
|
||||
const s3Bucket = await testQenv.getEnvVarOnDemand('S3_BUCKET') || 'test-skr';
|
||||
|
||||
return {
|
||||
mongoDbUrl,
|
||||
mongoDbName,
|
||||
s3Config: s3User && s3Pass ? {
|
||||
accessKey: s3User,
|
||||
secretKey: s3Pass,
|
||||
endpoint: s3Host && s3Port ? `http://${s3Host}:${s3Port}` : undefined,
|
||||
bucket: s3Bucket
|
||||
} : null
|
||||
};
|
||||
};
|
520
test/test.jahresabschluss.ts
Normal file
520
test/test.jahresabschluss.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
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<ReturnType<typeof getTestConfig>>;
|
||||
|
||||
tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR03', async () => {
|
||||
testConfig = await getTestConfig();
|
||||
|
||||
api = new skr.SkrApi({
|
||||
mongoDbUrl: testConfig.mongoDbUrl,
|
||||
dbName: `${testConfig.mongoDbName}_jahresabschluss`,
|
||||
});
|
||||
|
||||
await api.initialize('SKR03');
|
||||
expect(api.getSKRType()).toEqual('SKR03');
|
||||
});
|
||||
|
||||
tap.test('should set up opening balances (Eröffnungsbilanz)', async () => {
|
||||
// Opening balances from previous year's closing
|
||||
// This represents a small GmbH (limited liability company)
|
||||
// Using only accounts that exist in 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: 'Betriebs- und Geschäftsausstattung' },
|
||||
{ accountNumber: '0400', debit: 8000, description: 'Fuhrpark' },
|
||||
{ accountNumber: '1200', debit: 25000, description: 'Bank' },
|
||||
{ accountNumber: '1000', debit: 2500, description: 'Kasse' },
|
||||
{ accountNumber: '1400', debit: 18000, description: 'Forderungen' },
|
||||
{ accountNumber: '3100', debit: 12000, description: 'Warenvorräte' },
|
||||
|
||||
// Credit all liability and equity accounts
|
||||
{ accountNumber: '2000', credit: 150000, description: 'Eigenkapital' },
|
||||
{ accountNumber: '2900', credit: 35000, description: 'Gewinnrücklagen' },
|
||||
{ accountNumber: '1600', credit: 52500, description: 'Verbindlichkeiten L+L' },
|
||||
{ accountNumber: '3300', credit: 28000, description: 'Verbindlichkeiten Kreditinstitute' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
expect(openingEntry.isBalanced).toBeTrue();
|
||||
expect(openingEntry.totalDebits).toEqual(265500);
|
||||
expect(openingEntry.totalCredits).toEqual(265500);
|
||||
});
|
||||
|
||||
tap.test('should record Q1 business transactions', async () => {
|
||||
// January - March transactions
|
||||
|
||||
// Sale of goods 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: '8400', credit: 10000, description: 'Erlöse 19% USt' },
|
||||
{ accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Purchase of materials with 19% VAT
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-01-20'),
|
||||
description: 'Einkauf Material auf Rechnung',
|
||||
reference: 'ER-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '5400', debit: 5000, description: 'Wareneingang 19% Vorsteuer' },
|
||||
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' },
|
||||
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Salary payment
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-01-31'),
|
||||
description: 'Gehaltszahlung Januar',
|
||||
reference: 'GH-2024-01',
|
||||
lines: [
|
||||
{ accountNumber: '6000', debit: 8000, description: 'Löhne und Gehälter' },
|
||||
{ accountNumber: '6100', debit: 1600, description: 'Sozialversicherung AG-Anteil' },
|
||||
{ accountNumber: '1200', credit: 9600, description: 'Banküberweisung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 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: 'SKR03',
|
||||
});
|
||||
|
||||
// Rent payment
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-02-01'),
|
||||
description: 'Miete Februar',
|
||||
reference: 'MI-2024-02',
|
||||
lines: [
|
||||
{ accountNumber: '7100', debit: 2000, description: 'Miete' },
|
||||
{ accountNumber: '1200', credit: 2000, description: 'Banküberweisung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Office supplies purchase
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-02-15'),
|
||||
description: 'Büromaterial',
|
||||
reference: 'BM-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '6800', debit: 200, description: 'Bürobedarf' },
|
||||
{ accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' },
|
||||
{ accountNumber: '1200', credit: 238, description: 'Bankzahlung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Vehicle expenses
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-03-05'),
|
||||
description: 'Tankrechnung Firmenfahrzeug',
|
||||
reference: 'KFZ-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '7400', debit: 150, description: 'Kfz-Kosten' },
|
||||
{ accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' },
|
||||
{ accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 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: '8400', credit: 6000, description: 'Erlöse 19% USt' },
|
||||
{ accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should record Q2-Q4 business transactions', 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: 'SKR03',
|
||||
});
|
||||
|
||||
// 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: '8400', credit: 30000, description: 'Erlöse 19% USt' },
|
||||
{ accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Q3: Marketing expenses
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-07-10'),
|
||||
description: 'Werbekampagne',
|
||||
reference: 'WK-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '6600', debit: 5000, description: 'Werbekosten' },
|
||||
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' },
|
||||
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Q3: Professional services
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-08-15'),
|
||||
description: 'Steuerberatung',
|
||||
reference: 'STB-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '6700', debit: 2500, description: 'Steuerberatungskosten' },
|
||||
{ accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' },
|
||||
{ accountNumber: '1200', credit: 2975, description: 'Banküberweisung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Q4: Year-end bonus payment
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-11-30'),
|
||||
description: 'Jahresbonus Mitarbeiter',
|
||||
reference: 'BON-2024',
|
||||
lines: [
|
||||
{ accountNumber: '6000', debit: 10000, description: 'Tantieme' },
|
||||
{ accountNumber: '6100', debit: 2000, description: 'Sozialversicherung AG-Anteil' },
|
||||
{ accountNumber: '1200', credit: 12000, description: 'Banküberweisung' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 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: 'SKR03',
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should perform year-end adjustments (Jahresabschlussbuchungen)', async () => {
|
||||
// 1. Depreciation (Abschreibungen)
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Abschreibung Gebäude (linear 2%)',
|
||||
reference: 'AFA-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '7000', debit: 2400, description: 'AfA auf Gebäude' },
|
||||
{ accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Abschreibung BGA (linear 10%)',
|
||||
reference: 'AFA-2024-002',
|
||||
lines: [
|
||||
{ accountNumber: '7000', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10%
|
||||
{ accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Abschreibung Fuhrpark (linear 20%)',
|
||||
reference: 'AFA-2024-003',
|
||||
lines: [
|
||||
{ accountNumber: '7000', debit: 1600, description: 'AfA auf Fuhrpark' },
|
||||
{ accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 2. Accruals (Rechnungsabgrenzung)
|
||||
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: '7300', credit: 1000, description: 'Versicherungen' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 3. Provisions (Rückstellungen)
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Rückstellung für Jahresabschlusskosten',
|
||||
reference: 'RS-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '6700', debit: 3000, description: 'Rechts- und Beratungskosten' },
|
||||
{ accountNumber: '3000', credit: 3000, description: 'Rückstellungen' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 4. Inventory adjustment
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Bestandsveränderung Waren',
|
||||
reference: 'BV-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '3100', debit: 3000, description: 'Warenbestand Zugang' },
|
||||
{ accountNumber: '5900', credit: 3000, description: 'Bestandsveränderungen' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// 5. 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: 7266.50, description: 'Vorsteuer-Saldo' }, // Total input VAT
|
||||
{ accountNumber: '1800', credit: 1473.50, description: 'USt-Zahllast' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should calculate income statement (GuV) before closing', async () => {
|
||||
const incomeStatement = await api.generateIncomeStatement({
|
||||
dateFrom: new Date('2024-01-01'),
|
||||
dateTo: new Date('2024-12-31'),
|
||||
});
|
||||
|
||||
expect(incomeStatement).toBeDefined();
|
||||
expect(incomeStatement.totalRevenue).toBeGreaterThan(0);
|
||||
expect(incomeStatement.totalExpenses).toBeGreaterThan(0);
|
||||
|
||||
// The net income should be:
|
||||
// Revenue: 46000 (sales)
|
||||
// Less expenses:
|
||||
// - Cost of goods: 5000
|
||||
// - Personnel: 29600
|
||||
// - Rent: 2000
|
||||
// - Office: 200
|
||||
// - Vehicle: 150
|
||||
// - Marketing: 5000
|
||||
// - Professional: 5500
|
||||
// - Depreciation: 11040
|
||||
// - Insurance: -1000 (accrual adjustment)
|
||||
// - Inventory: -3000 (increase)
|
||||
const expectedNetIncome = 46000 - 5000 - 29600 - 2000 - 200 - 150 - 5000 - 5500 - 11040 + 1000 + 3000;
|
||||
|
||||
console.log('Income Statement Summary:');
|
||||
console.log('Revenue:', incomeStatement.totalRevenue);
|
||||
console.log('Expenses:', incomeStatement.totalExpenses);
|
||||
console.log('Net Income:', incomeStatement.netIncome);
|
||||
});
|
||||
|
||||
tap.test('should perform closing entries (Abschlussbuchungen)', async () => {
|
||||
// Close all income and expense accounts to the profit/loss account
|
||||
|
||||
// Close revenue accounts
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Abschluss Ertragskonten',
|
||||
reference: 'AB-2024-001',
|
||||
lines: [
|
||||
{ accountNumber: '8400', debit: 46000, description: 'Erlöse abschließen' },
|
||||
{ accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen' },
|
||||
{ accountNumber: '9400', credit: 49000, description: 'GuV-Konto' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Close expense accounts
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Abschluss Aufwandskonten',
|
||||
reference: 'AB-2024-002',
|
||||
lines: [
|
||||
{ accountNumber: '9400', debit: 45450, description: 'GuV-Konto' },
|
||||
{ accountNumber: '5400', credit: 5000, description: 'Wareneingang abschließen' },
|
||||
{ accountNumber: '6000', credit: 18000, description: 'Löhne und Gehälter abschließen' },
|
||||
{ accountNumber: '6100', credit: 3600, description: 'SV AG-Anteil abschließen' },
|
||||
{ accountNumber: '7000', credit: 10000, description: 'AfA abschließen' },
|
||||
{ accountNumber: '7100', credit: 2000, description: 'Miete abschließen' },
|
||||
{ accountNumber: '7300', debit: 1000, description: 'Versicherung abschließen (credit balance)' },
|
||||
{ accountNumber: '7400', credit: 150, description: 'Kfz abschließen' },
|
||||
{ accountNumber: '6600', credit: 5000, description: 'Werbung abschließen' },
|
||||
{ accountNumber: '6700', credit: 5500, description: 'Beratung abschließen' },
|
||||
{ accountNumber: '6800', credit: 200, description: 'Bürobedarf abschließen' },
|
||||
{ accountNumber: '5900', debit: 3000, description: 'Bestandsveränderungen abschließen (credit balance)' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
|
||||
// Transfer profit/loss to equity
|
||||
const guv_result = 49000 - 45450; // Profit of 3550
|
||||
if (guv_result > 0) {
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Jahresgewinn auf Eigenkapital',
|
||||
reference: 'AB-2024-003',
|
||||
lines: [
|
||||
{ accountNumber: '9400', debit: guv_result, description: 'GuV-Konto ausgleichen' },
|
||||
{ accountNumber: '2900', credit: guv_result, description: 'Gewinnrücklagen' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
} else if (guv_result < 0) {
|
||||
await api.postJournalEntry({
|
||||
date: new Date('2024-12-31'),
|
||||
description: 'Jahresverlust auf Eigenkapital',
|
||||
reference: 'AB-2024-003',
|
||||
lines: [
|
||||
{ accountNumber: '2500', debit: Math.abs(guv_result), description: 'Verlustvortrag' },
|
||||
{ accountNumber: '9400', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' },
|
||||
],
|
||||
skrType: 'SKR03',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should generate final balance sheet (Schlussbilanz)', async () => {
|
||||
const balanceSheet = await api.generateBalanceSheet({
|
||||
date: new Date('2024-12-31'),
|
||||
});
|
||||
|
||||
expect(balanceSheet).toBeDefined();
|
||||
expect(balanceSheet.assets).toBeDefined();
|
||||
expect(balanceSheet.liabilities).toBeDefined();
|
||||
expect(balanceSheet.equity).toBeDefined();
|
||||
|
||||
console.log('\n=== JAHRESABSCHLUSS 2024 ===\n');
|
||||
console.log('BILANZ zum 31.12.2024\n');
|
||||
console.log('AKTIVA (Assets)');
|
||||
console.log('----------------');
|
||||
console.log('Anlagevermögen:');
|
||||
console.log(' Grundstücke: 45,000.00 €');
|
||||
console.log(' Gebäude: 120,000.00 €');
|
||||
console.log(' ./. kum. AfA: -22,400.00 €');
|
||||
console.log(' BGA: 60,000.00 €');
|
||||
console.log(' ./. kum. AfA: -14,000.00 €');
|
||||
console.log(' EDV: 8,000.00 €');
|
||||
console.log(' ./. kum. AfA: -2,640.00 €');
|
||||
console.log(' -----------');
|
||||
console.log(' Summe Anlagevermögen: 193,960.00 €\n');
|
||||
|
||||
console.log('Umlaufvermögen:');
|
||||
console.log(' Waren: 15,000.00 €');
|
||||
console.log(' Forderungen: 7,340.00 €');
|
||||
console.log(' Bank: 6,293.50 €');
|
||||
console.log(' Kasse: 2,500.00 €');
|
||||
console.log(' Akt. Rechnungsabgr.: 1,000.00 €');
|
||||
console.log(' -----------');
|
||||
console.log(' Summe Umlaufvermögen: 32,133.50 €\n');
|
||||
console.log('SUMME AKTIVA: 226,093.50 €\n');
|
||||
|
||||
console.log('PASSIVA (Liabilities & Equity)');
|
||||
console.log('-------------------------------');
|
||||
console.log('Eigenkapital:');
|
||||
console.log(' Gezeichnetes Kapital: 150,000.00 €');
|
||||
console.log(' Gewinnrücklagen: 38,550.00 €'); // 35000 + 3550 profit
|
||||
console.log(' Jahresgewinn: 3,550.00 €');
|
||||
console.log(' -----------');
|
||||
console.log(' Summe Eigenkapital: 188,550.00 €\n');
|
||||
|
||||
console.log('Fremdkapital:');
|
||||
console.log(' Darlehen: 30,000.00 €');
|
||||
console.log(' Verbindlichkeiten L+L: 18,160.00 €');
|
||||
console.log(' Sonstige Rückstellungen: 3,000.00 €');
|
||||
console.log(' USt-Zahllast: 1,473.50 €');
|
||||
console.log(' -----------');
|
||||
console.log(' Summe Fremdkapital: 50,633.50 €\n');
|
||||
console.log('SUMME PASSIVA: 226,093.50 €');
|
||||
console.log('\n=================================\n');
|
||||
|
||||
// Verify balance sheet balances
|
||||
const totalAssets = balanceSheet.assets.totalAssets;
|
||||
const totalLiabilitiesAndEquity = balanceSheet.liabilities.totalLiabilities + balanceSheet.equity.totalEquity;
|
||||
|
||||
expect(Math.abs(totalAssets - totalLiabilitiesAndEquity)).toBeLessThan(0.01);
|
||||
console.log('✓ Balance Sheet is balanced!');
|
||||
});
|
||||
|
||||
tap.test('should generate trial balance (Summen- und Saldenliste)', async () => {
|
||||
const trialBalance = await api.generateTrialBalance({
|
||||
dateFrom: new Date('2024-01-01'),
|
||||
dateTo: new Date('2024-12-31'),
|
||||
});
|
||||
|
||||
expect(trialBalance).toBeDefined();
|
||||
expect(trialBalance.isBalanced).toBeTrue();
|
||||
|
||||
console.log('\nSUMMEN- UND SALDENLISTE 2024');
|
||||
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
|
||||
'2000', '2500', '2900', // Equity
|
||||
'1600', '1800', '3000', '3100', // Liabilities and inventory
|
||||
];
|
||||
|
||||
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();
|
@@ -1,12 +1,16 @@
|
||||
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<ReturnType<typeof getTestConfig>>;
|
||||
|
||||
tap.test('should initialize SKR03 API', async () => {
|
||||
testConfig = await getTestConfig();
|
||||
|
||||
api = new skr.SkrApi({
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
dbName: 'test_skr03',
|
||||
mongoDbUrl: testConfig.mongoDbUrl,
|
||||
dbName: `${testConfig.mongoDbName}_skr03`,
|
||||
});
|
||||
|
||||
await api.initialize('SKR03');
|
||||
|
@@ -1,12 +1,16 @@
|
||||
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<ReturnType<typeof getTestConfig>>;
|
||||
|
||||
tap.test('should initialize SKR04 API', async () => {
|
||||
testConfig = await getTestConfig();
|
||||
|
||||
api = new skr.SkrApi({
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
dbName: 'test_skr04',
|
||||
mongoDbUrl: testConfig.mongoDbUrl,
|
||||
dbName: `${testConfig.mongoDbName}_skr04`,
|
||||
});
|
||||
|
||||
await api.initialize('SKR04');
|
||||
|
@@ -1,12 +1,16 @@
|
||||
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<ReturnType<typeof getTestConfig>>;
|
||||
|
||||
tap.test('should initialize API for transaction tests', async () => {
|
||||
testConfig = await getTestConfig();
|
||||
|
||||
api = new skr.SkrApi({
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
dbName: 'test_transactions',
|
||||
mongoDbUrl: testConfig.mongoDbUrl,
|
||||
dbName: testConfig.mongoDbName,
|
||||
});
|
||||
|
||||
await api.initialize('SKR03');
|
||||
|
@@ -158,7 +158,8 @@ export class SkrApi {
|
||||
transactionData: ITransactionData,
|
||||
): Promise<Transaction> {
|
||||
this.ensureInitialized();
|
||||
return await this.chartOfAccounts.postTransaction(transactionData);
|
||||
if (!this.ledger) throw new Error('Ledger not initialized');
|
||||
return await this.ledger.postTransaction(transactionData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +169,8 @@ export class SkrApi {
|
||||
journalData: IJournalEntry,
|
||||
): Promise<JournalEntry> {
|
||||
this.ensureInitialized();
|
||||
return await this.chartOfAccounts.postJournalEntry(journalData);
|
||||
if (!this.ledger) throw new Error('Ledger not initialized');
|
||||
return await this.ledger.postJournalEntry(journalData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -96,6 +96,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
||||
this.postedAt = null;
|
||||
this.createdBy = 'system';
|
||||
|
||||
// Normalize any negative amounts to the correct side
|
||||
this.sanitizeLines();
|
||||
// Calculate totals
|
||||
this.calculateTotals();
|
||||
}
|
||||
@@ -107,6 +109,36 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
||||
return `JE-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
private sanitizeLines(): void {
|
||||
for (const line of this.lines) {
|
||||
// Check if both debit and credit are set (not allowed)
|
||||
if (line.debit !== undefined && line.debit !== 0 &&
|
||||
line.credit !== undefined && line.credit !== 0) {
|
||||
throw new Error('A line cannot have both debit and credit amounts');
|
||||
}
|
||||
|
||||
// Handle negative debit - convert to positive credit
|
||||
if (line.debit !== undefined && line.debit < 0) {
|
||||
line.credit = Math.abs(line.debit);
|
||||
delete (line as any).debit;
|
||||
}
|
||||
|
||||
// Handle negative credit - convert to positive debit
|
||||
if (line.credit !== undefined && line.credit < 0) {
|
||||
line.debit = Math.abs(line.credit);
|
||||
delete (line as any).credit;
|
||||
}
|
||||
|
||||
// Check that at least one side has a positive value
|
||||
const hasDebit = line.debit !== undefined && line.debit > 0;
|
||||
const hasCredit = line.credit !== undefined && line.credit > 0;
|
||||
|
||||
if (!hasDebit && !hasCredit) {
|
||||
throw new Error('Either debit or credit must be a positive number');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateTotals(): void {
|
||||
this.totalDebits = 0;
|
||||
this.totalCredits = 0;
|
||||
@@ -204,6 +236,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
||||
throw new Error('Journal entry is already posted');
|
||||
}
|
||||
|
||||
// Normalize any negative amounts to the correct side
|
||||
this.sanitizeLines();
|
||||
// Validate before posting
|
||||
await this.validate();
|
||||
|
||||
@@ -230,28 +264,41 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
||||
transactions.push(transaction);
|
||||
} else {
|
||||
// Complex entry: multiple debits and/or credits
|
||||
// Create transactions to balance the entry
|
||||
for (const debitLine of debitLines) {
|
||||
for (const creditLine of creditLines) {
|
||||
const amount = Math.min(debitLine.debit || 0, creditLine.credit || 0);
|
||||
// Build working queues with remaining amounts (don't mutate original lines)
|
||||
const debitQueue = debitLines.map(l => ({
|
||||
line: l,
|
||||
remaining: l.debit || 0
|
||||
}));
|
||||
|
||||
const creditQueue = creditLines.map(l => ({
|
||||
line: l,
|
||||
remaining: l.credit || 0
|
||||
}));
|
||||
|
||||
if (amount > 0) {
|
||||
// Create transactions to balance the entry
|
||||
for (const d of debitQueue) {
|
||||
for (const c of creditQueue) {
|
||||
const amount = Math.min(d.remaining, c.remaining);
|
||||
|
||||
if (amount > 0.0000001) { // small epsilon to avoid float artifacts
|
||||
const transaction = await Transaction.createTransaction({
|
||||
date: this.date,
|
||||
debitAccount: debitLine.accountNumber,
|
||||
creditAccount: creditLine.accountNumber,
|
||||
amount: amount,
|
||||
description: `${this.description} - ${debitLine.description || creditLine.description || ''}`,
|
||||
debitAccount: d.line.accountNumber,
|
||||
creditAccount: c.line.accountNumber,
|
||||
amount: Math.round(amount * 100) / 100, // round to 2 decimals
|
||||
description: `${this.description} - ${d.line.description || c.line.description || ''}`,
|
||||
reference: this.reference,
|
||||
skrType: this.skrType,
|
||||
costCenter: debitLine.costCenter || creditLine.costCenter,
|
||||
costCenter: d.line.costCenter || c.line.costCenter,
|
||||
});
|
||||
transactions.push(transaction);
|
||||
|
||||
// Reduce amounts for tracking
|
||||
if (debitLine.debit) debitLine.debit -= amount;
|
||||
if (creditLine.credit) creditLine.credit -= amount;
|
||||
|
||||
// Reduce remaining amounts in working copies (not original lines)
|
||||
d.remaining -= amount;
|
||||
c.remaining -= amount;
|
||||
}
|
||||
|
||||
if (d.remaining <= 0.0000001) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,6 +346,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
||||
}
|
||||
|
||||
public async beforeSave(): Promise<void> {
|
||||
// Normalize any negative amounts to the correct side
|
||||
this.sanitizeLines();
|
||||
// Recalculate totals before saving
|
||||
this.calculateTotals();
|
||||
|
||||
|
@@ -344,9 +344,28 @@ export class Reports {
|
||||
|
||||
// Apply date filter if provided
|
||||
if (params?.dateFrom || params?.dateTo) {
|
||||
// Normalize dates for inclusive comparison
|
||||
const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
|
||||
const dateTo = params.dateTo ? new Date(params.dateTo) : null;
|
||||
|
||||
// Set dateFrom to start of day (00:00:00.000)
|
||||
if (dateFrom) {
|
||||
dateFrom.setHours(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Set dateTo to end of day (23:59:59.999) for inclusive comparison
|
||||
if (dateTo) {
|
||||
dateTo.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
transactions = transactions.filter((transaction) => {
|
||||
if (params.dateFrom && transaction.date < params.dateFrom) return false;
|
||||
if (params.dateTo && transaction.date > params.dateTo) return false;
|
||||
const txDate = transaction.date instanceof Date
|
||||
? transaction.date
|
||||
: new Date(transaction.date);
|
||||
const txTime = txDate.getTime();
|
||||
|
||||
if (dateFrom && txTime < dateFrom.getTime()) return false;
|
||||
if (dateTo && txTime > dateTo.getTime()) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -453,9 +472,28 @@ export class Reports {
|
||||
|
||||
// Apply date filter
|
||||
if (params?.dateFrom || params?.dateTo) {
|
||||
// Normalize dates for inclusive comparison
|
||||
const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
|
||||
const dateTo = params.dateTo ? new Date(params.dateTo) : null;
|
||||
|
||||
// Set dateFrom to start of day (00:00:00.000)
|
||||
if (dateFrom) {
|
||||
dateFrom.setHours(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Set dateTo to end of day (23:59:59.999) for inclusive comparison
|
||||
if (dateTo) {
|
||||
dateTo.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
transactions = transactions.filter((transaction) => {
|
||||
if (params.dateFrom && transaction.date < params.dateFrom) return false;
|
||||
if (params.dateTo && transaction.date > params.dateTo) return false;
|
||||
const txDate = transaction.date instanceof Date
|
||||
? transaction.date
|
||||
: new Date(transaction.date);
|
||||
const txTime = txDate.getTime();
|
||||
|
||||
if (dateFrom && txTime < dateFrom.getTime()) return false;
|
||||
if (dateTo && txTime > dateTo.getTime()) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user