- 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
534 lines
14 KiB
TypeScript
534 lines
14 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
|
|
import { Ledger } from './skr.classes.ledger.js';
|
|
import { Reports } from './skr.classes.reports.js';
|
|
import { Account } from './skr.classes.account.js';
|
|
import { Transaction } from './skr.classes.transaction.js';
|
|
import { JournalEntry } from './skr.classes.journalentry.js';
|
|
import type {
|
|
IDatabaseConfig,
|
|
TSKRType,
|
|
IAccountData,
|
|
IAccountFilter,
|
|
ITransactionData,
|
|
ITransactionFilter,
|
|
IJournalEntry,
|
|
IReportParams,
|
|
ITrialBalanceReport,
|
|
IIncomeStatement,
|
|
IBalanceSheet,
|
|
} from './skr.types.js';
|
|
|
|
/**
|
|
* Main API class for SKR accounting operations
|
|
*/
|
|
export class SkrApi {
|
|
private chartOfAccounts: ChartOfAccounts;
|
|
private ledger: Ledger | null = null;
|
|
private reports: Reports | null = null;
|
|
private logger: plugins.smartlog.Smartlog;
|
|
private initialized: boolean = false;
|
|
private currentSKRType: TSKRType | null = null;
|
|
|
|
constructor(private config: IDatabaseConfig) {
|
|
this.chartOfAccounts = new ChartOfAccounts(config);
|
|
this.logger = new plugins.smartlog.Smartlog({
|
|
logContext: {
|
|
company: 'fin.cx',
|
|
companyunit: 'skr',
|
|
containerName: 'SkrApi',
|
|
environment: 'local',
|
|
runtime: 'node',
|
|
zone: 'local',
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the API with specified SKR type
|
|
*/
|
|
public async initialize(skrType: TSKRType): Promise<void> {
|
|
this.logger.log('info', `Initializing SKR API with ${skrType}`);
|
|
|
|
// Initialize chart of accounts
|
|
if (skrType === 'SKR03') {
|
|
await this.chartOfAccounts.initializeSKR03();
|
|
} else if (skrType === 'SKR04') {
|
|
await this.chartOfAccounts.initializeSKR04();
|
|
} else {
|
|
throw new Error(`Invalid SKR type: ${skrType}`);
|
|
}
|
|
|
|
this.currentSKRType = skrType;
|
|
this.ledger = new Ledger(skrType);
|
|
this.reports = new Reports(skrType);
|
|
this.initialized = true;
|
|
|
|
this.logger.log('info', 'SKR API initialized successfully');
|
|
}
|
|
|
|
/**
|
|
* Ensure API is initialized
|
|
*/
|
|
private ensureInitialized(): void {
|
|
if (!this.initialized || !this.currentSKRType) {
|
|
throw new Error('API not initialized. Call initialize() first.');
|
|
}
|
|
}
|
|
|
|
// ========== Account Management ==========
|
|
|
|
/**
|
|
* Create a new account
|
|
*/
|
|
public async createAccount(
|
|
accountData: Partial<IAccountData>,
|
|
): Promise<Account> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.createCustomAccount(accountData);
|
|
}
|
|
|
|
/**
|
|
* Get account by number
|
|
*/
|
|
public async getAccount(accountNumber: string): Promise<Account | null> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getAccountByNumber(accountNumber);
|
|
}
|
|
|
|
/**
|
|
* Update an account
|
|
*/
|
|
public async updateAccount(
|
|
accountNumber: string,
|
|
updates: Partial<IAccountData>,
|
|
): Promise<Account> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.updateAccount(accountNumber, updates);
|
|
}
|
|
|
|
/**
|
|
* Delete an account
|
|
*/
|
|
public async deleteAccount(accountNumber: string): Promise<void> {
|
|
this.ensureInitialized();
|
|
await this.chartOfAccounts.deleteAccount(accountNumber);
|
|
}
|
|
|
|
/**
|
|
* List accounts with optional filter
|
|
*/
|
|
public async listAccounts(filter?: IAccountFilter): Promise<Account[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getAllAccounts(filter);
|
|
}
|
|
|
|
/**
|
|
* Search accounts by term
|
|
*/
|
|
public async searchAccounts(searchTerm: string): Promise<Account[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.searchAccounts(searchTerm);
|
|
}
|
|
|
|
/**
|
|
* Get accounts by class
|
|
*/
|
|
public async getAccountsByClass(accountClass: number): Promise<Account[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getAccountsByClass(accountClass);
|
|
}
|
|
|
|
/**
|
|
* Get accounts by type
|
|
*/
|
|
public async getAccountsByType(
|
|
accountType: IAccountData['accountType'],
|
|
): Promise<Account[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getAccountsByType(accountType);
|
|
}
|
|
|
|
// ========== Transaction Management ==========
|
|
|
|
/**
|
|
* Post a simple transaction
|
|
*/
|
|
public async postTransaction(
|
|
transactionData: ITransactionData,
|
|
): Promise<Transaction> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.postTransaction(transactionData);
|
|
}
|
|
|
|
/**
|
|
* Post a journal entry
|
|
*/
|
|
public async postJournalEntry(
|
|
journalData: IJournalEntry,
|
|
): Promise<JournalEntry> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.postJournalEntry(journalData);
|
|
}
|
|
|
|
/**
|
|
* Get transaction by ID
|
|
*/
|
|
public async getTransaction(
|
|
transactionId: string,
|
|
): Promise<Transaction | null> {
|
|
this.ensureInitialized();
|
|
return await Transaction.getTransactionById(transactionId);
|
|
}
|
|
|
|
/**
|
|
* List transactions with optional filter
|
|
*/
|
|
public async listTransactions(
|
|
filter?: ITransactionFilter,
|
|
): Promise<Transaction[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getTransactions(filter);
|
|
}
|
|
|
|
/**
|
|
* Get transactions for specific account
|
|
*/
|
|
public async getAccountTransactions(
|
|
accountNumber: string,
|
|
): Promise<Transaction[]> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.getAccountTransactions(accountNumber);
|
|
}
|
|
|
|
/**
|
|
* Reverse a transaction
|
|
*/
|
|
public async reverseTransaction(transactionId: string): Promise<Transaction> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.reverseTransaction(transactionId);
|
|
}
|
|
|
|
/**
|
|
* Reverse a journal entry
|
|
*/
|
|
public async reverseJournalEntry(journalId: string): Promise<JournalEntry> {
|
|
this.ensureInitialized();
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
return await this.ledger.reverseJournalEntry(journalId);
|
|
}
|
|
|
|
// ========== Reporting ==========
|
|
|
|
/**
|
|
* Generate trial balance
|
|
*/
|
|
public async generateTrialBalance(
|
|
params?: IReportParams,
|
|
): Promise<ITrialBalanceReport> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.getTrialBalance(params);
|
|
}
|
|
|
|
/**
|
|
* Generate income statement
|
|
*/
|
|
public async generateIncomeStatement(
|
|
params?: IReportParams,
|
|
): Promise<IIncomeStatement> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.getIncomeStatement(params);
|
|
}
|
|
|
|
/**
|
|
* Generate balance sheet
|
|
*/
|
|
public async generateBalanceSheet(
|
|
params?: IReportParams,
|
|
): Promise<IBalanceSheet> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.getBalanceSheet(params);
|
|
}
|
|
|
|
/**
|
|
* Generate general ledger
|
|
*/
|
|
public async generateGeneralLedger(params?: IReportParams): Promise<any> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.getGeneralLedger(params);
|
|
}
|
|
|
|
/**
|
|
* Generate cash flow statement
|
|
*/
|
|
public async generateCashFlowStatement(params?: IReportParams): Promise<any> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.getCashFlowStatement(params);
|
|
}
|
|
|
|
/**
|
|
* Export report to CSV
|
|
*/
|
|
public async exportReportToCSV(
|
|
reportType: 'trial_balance' | 'income_statement' | 'balance_sheet',
|
|
params?: IReportParams,
|
|
): Promise<string> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.exportToCSV(reportType, params);
|
|
}
|
|
|
|
/**
|
|
* Export to DATEV format
|
|
*/
|
|
public async exportToDATEV(params?: IReportParams): Promise<string> {
|
|
this.ensureInitialized();
|
|
if (!this.reports) throw new Error('Reports not initialized');
|
|
return await this.reports.exportToDATEV(params);
|
|
}
|
|
|
|
// ========== Period Management ==========
|
|
|
|
/**
|
|
* Close accounting period
|
|
*/
|
|
public async closePeriod(
|
|
period: string,
|
|
closingAccountNumber?: string,
|
|
): Promise<JournalEntry[]> {
|
|
this.ensureInitialized();
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
return await this.ledger.closeAccountingPeriod(
|
|
period,
|
|
closingAccountNumber,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get account balance
|
|
*/
|
|
public async getAccountBalance(
|
|
accountNumber: string,
|
|
asOfDate?: Date,
|
|
): Promise<any> {
|
|
this.ensureInitialized();
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
return await this.ledger.getAccountBalance(accountNumber, asOfDate);
|
|
}
|
|
|
|
/**
|
|
* Recalculate all account balances
|
|
*/
|
|
public async recalculateBalances(): Promise<void> {
|
|
this.ensureInitialized();
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
await this.ledger.recalculateAllBalances();
|
|
}
|
|
|
|
// ========== Import/Export ==========
|
|
|
|
/**
|
|
* Import accounts from CSV
|
|
*/
|
|
public async importAccountsFromCSV(csvContent: string): Promise<number> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.importAccountsFromCSV(csvContent);
|
|
}
|
|
|
|
/**
|
|
* Export accounts to CSV
|
|
*/
|
|
public async exportAccountsToCSV(): Promise<string> {
|
|
this.ensureInitialized();
|
|
return await this.chartOfAccounts.exportAccountsToCSV();
|
|
}
|
|
|
|
// ========== Utility Methods ==========
|
|
|
|
/**
|
|
* Get current SKR type
|
|
*/
|
|
public getSKRType(): TSKRType | null {
|
|
return this.currentSKRType;
|
|
}
|
|
|
|
/**
|
|
* Get account class description
|
|
*/
|
|
public getAccountClassDescription(accountClass: number): string {
|
|
this.ensureInitialized();
|
|
return this.chartOfAccounts.getAccountClassDescription(accountClass);
|
|
}
|
|
|
|
/**
|
|
* Validate double-entry rules
|
|
*/
|
|
public validateDoubleEntry(
|
|
debitAmount: number,
|
|
creditAmount: number,
|
|
): boolean {
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
return this.ledger.validateDoubleEntry(debitAmount, creditAmount);
|
|
}
|
|
|
|
/**
|
|
* Get unbalanced transactions (for audit)
|
|
*/
|
|
public async getUnbalancedTransactions(): Promise<Transaction[]> {
|
|
this.ensureInitialized();
|
|
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
return await this.ledger.getUnbalancedTransactions();
|
|
}
|
|
|
|
/**
|
|
* Close the API and database connection
|
|
*/
|
|
public async close(): Promise<void> {
|
|
await this.chartOfAccounts.close();
|
|
this.initialized = false;
|
|
this.currentSKRType = null;
|
|
this.ledger = null;
|
|
this.reports = null;
|
|
this.logger.log('info', 'SKR API closed');
|
|
}
|
|
|
|
// ========== Batch Operations ==========
|
|
|
|
/**
|
|
* Post multiple transactions
|
|
*/
|
|
public async postBatchTransactions(
|
|
transactions: ITransactionData[],
|
|
): Promise<Transaction[]> {
|
|
this.ensureInitialized();
|
|
|
|
const results: Transaction[] = [];
|
|
const errors: Array<{ index: number; error: string }> = [];
|
|
|
|
for (let i = 0; i < transactions.length; i++) {
|
|
try {
|
|
const transaction = await this.postTransaction(transactions[i]);
|
|
results.push(transaction);
|
|
} catch (error) {
|
|
errors.push({ index: i, error: error.message });
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
this.logger.log(
|
|
'warn',
|
|
`Batch transaction posting completed with ${errors.length} errors`,
|
|
);
|
|
throw new Error(
|
|
`Batch posting failed for ${errors.length} transactions: ${JSON.stringify(errors)}`,
|
|
);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Create multiple accounts
|
|
*/
|
|
public async createBatchAccounts(
|
|
accounts: IAccountData[],
|
|
): Promise<Account[]> {
|
|
this.ensureInitialized();
|
|
|
|
const results: Account[] = [];
|
|
const errors: Array<{ index: number; error: string }> = [];
|
|
|
|
for (let i = 0; i < accounts.length; i++) {
|
|
try {
|
|
const account = await this.createAccount(accounts[i]);
|
|
results.push(account);
|
|
} catch (error) {
|
|
errors.push({ index: i, error: error.message });
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
this.logger.log(
|
|
'warn',
|
|
`Batch account creation completed with ${errors.length} errors`,
|
|
);
|
|
throw new Error(
|
|
`Batch creation failed for ${errors.length} accounts: ${JSON.stringify(errors)}`,
|
|
);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ========== Pagination Support ==========
|
|
|
|
/**
|
|
* Get paginated accounts
|
|
*/
|
|
public async getAccountsPaginated(
|
|
page: number = 1,
|
|
pageSize: number = 50,
|
|
filter?: IAccountFilter,
|
|
): Promise<{
|
|
data: Account[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
totalPages: number;
|
|
}> {
|
|
this.ensureInitialized();
|
|
|
|
const allAccounts = await this.listAccounts(filter);
|
|
const total = allAccounts.length;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
const start = (page - 1) * pageSize;
|
|
const end = start + pageSize;
|
|
const data = allAccounts.slice(start, end);
|
|
|
|
return {
|
|
data,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
totalPages,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get paginated transactions
|
|
*/
|
|
public async getTransactionsPaginated(
|
|
page: number = 1,
|
|
pageSize: number = 50,
|
|
filter?: ITransactionFilter,
|
|
): Promise<{
|
|
data: Transaction[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
totalPages: number;
|
|
}> {
|
|
this.ensureInitialized();
|
|
|
|
const allTransactions = await this.listTransactions(filter);
|
|
const total = allTransactions.length;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
const start = (page - 1) * pageSize;
|
|
const end = start + pageSize;
|
|
const data = allTransactions.slice(start, end);
|
|
|
|
return {
|
|
data,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
totalPages,
|
|
};
|
|
}
|
|
}
|