import * as plugins from './plugins.js'; import * as path from 'path'; 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 { SkrExport, type IExportOptions } from './skr.export.js'; import { LedgerExporter } from './skr.export.ledger.js'; import { AccountsExporter } from './skr.export.accounts.js'; import { BalancesExporter } from './skr.export.balances.js'; import { PdfReportGenerator, type IPdfReportOptions } from './skr.export.pdf.js'; import { SecurityManager, type ISigningOptions } from './skr.security.js'; import { InvoiceAdapter } from './skr.invoice.adapter.js'; import { InvoiceStorage } from './skr.invoice.storage.js'; import { InvoiceBookingEngine, type IBookingOptions, type IBookingResult } from './skr.invoice.booking.js'; import type { IInvoice, IInvoiceFilter, IInvoiceImportOptions, IInvoiceExportOptions, IBookingRules, TInvoiceDirection, } from './skr.invoice.entity.js'; import type { IDatabaseConfig, TSKRType, IAccountData, IAccountFilter, ITransactionData, ITransactionFilter, IJournalEntry, IReportParams, ITrialBalanceReport, IIncomeStatement, IBalanceSheet, IAccountBalance, } 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; private invoiceAdapter: InvoiceAdapter | null = null; private invoiceStorage: InvoiceStorage | null = null; private invoiceBookingEngine: InvoiceBookingEngine | 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 { 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); // Initialize invoice components this.invoiceAdapter = new InvoiceAdapter(); const invoicePath = this.config.invoiceExportPath || path.resolve(process.cwd(), 'exports', 'invoices'); this.invoiceStorage = new InvoiceStorage(invoicePath); this.invoiceBookingEngine = new InvoiceBookingEngine(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, ): Promise { this.ensureInitialized(); return await this.chartOfAccounts.createCustomAccount(accountData); } /** * Get account by number */ public async getAccount(accountNumber: string): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getAccountByNumber(accountNumber); } /** * Update an account */ public async updateAccount( accountNumber: string, updates: Partial, ): Promise { this.ensureInitialized(); return await this.chartOfAccounts.updateAccount(accountNumber, updates); } /** * Delete an account */ public async deleteAccount(accountNumber: string): Promise { this.ensureInitialized(); await this.chartOfAccounts.deleteAccount(accountNumber); } /** * List accounts with optional filter */ public async listAccounts(filter?: IAccountFilter): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getAllAccounts(filter); } /** * Search accounts by term */ public async searchAccounts(searchTerm: string): Promise { this.ensureInitialized(); return await this.chartOfAccounts.searchAccounts(searchTerm); } /** * Get accounts by class */ public async getAccountsByClass(accountClass: number): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getAccountsByClass(accountClass); } /** * Get accounts by type */ public async getAccountsByType( accountType: IAccountData['accountType'], ): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getAccountsByType(accountType); } // ========== Transaction Management ========== /** * Post a simple transaction */ public async postTransaction( transactionData: ITransactionData, ): Promise { this.ensureInitialized(); if (!this.ledger) throw new Error('Ledger not initialized'); return await this.ledger.postTransaction(transactionData); } /** * Post a journal entry */ public async postJournalEntry( journalData: IJournalEntry, ): Promise { this.ensureInitialized(); if (!this.ledger) throw new Error('Ledger not initialized'); return await this.ledger.postJournalEntry(journalData); } /** * Get transaction by ID */ public async getTransaction( transactionId: string, ): Promise { this.ensureInitialized(); return await Transaction.getTransactionById(transactionId); } /** * List transactions with optional filter */ public async listTransactions( filter?: ITransactionFilter, ): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getTransactions(filter); } /** * Get transactions for specific account */ public async getAccountTransactions( accountNumber: string, ): Promise { this.ensureInitialized(); return await this.chartOfAccounts.getAccountTransactions(accountNumber); } /** * Reverse a transaction */ public async reverseTransaction(transactionId: string): Promise { this.ensureInitialized(); return await this.chartOfAccounts.reverseTransaction(transactionId); } /** * Reverse a journal entry */ public async reverseJournalEntry(journalId: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { this.ensureInitialized(); return await this.chartOfAccounts.importAccountsFromCSV(csvContent); } /** * Export accounts to CSV */ public async exportAccountsToCSV(): Promise { this.ensureInitialized(); return await this.chartOfAccounts.exportAccountsToCSV(); } /** * Export Jahresabschluss in GoBD-compliant BagIt format * Creates a revision-safe export for 10-year archival */ public async exportJahresabschluss(options: IExportOptions): Promise { this.ensureInitialized(); if (!this.ledger || !this.reports || !this.currentSKRType) { throw new Error('API not fully initialized'); } this.logger.log('info', `Starting Jahresabschluss export for fiscal year ${options.fiscalYear}`); // Create export instance const exporter = new SkrExport(options); // Create BagIt structure await exporter.createBagItStructure(); await exporter.createExportMetadata(this.currentSKRType); await exporter.createSchemas(); // Export accounting data await this.exportLedgerData(exporter, options); await this.exportAccountData(exporter, options); await this.exportBalanceData(exporter, options); // Generate PDF reports if requested if (options.generatePdfReports) { await this.generatePdfReports(exporter, options); } // Sign export if requested if (options.signExport) { await this.signExport(exporter, options); } // Create manifests and validate await exporter.writeManifests(); const merkleRoot = await exporter.createMerkleTree(); const isValid = await exporter.validateBagIt(); if (!isValid) { throw new Error('BagIt validation failed'); } this.logger.log('ok', `Jahresabschluss export completed. Merkle root: ${merkleRoot}`); return options.exportPath; } /** * Export ledger data in NDJSON format */ private async exportLedgerData(exporter: SkrExport, options: IExportOptions): Promise { if (!this.ledger) throw new Error('Ledger not initialized'); const ledgerExporter = new LedgerExporter(options.exportPath); await ledgerExporter.initialize(); // Get all transactions for the period const transactions = await this.chartOfAccounts.getTransactions({ dateFrom: options.dateFrom, dateTo: options.dateTo }); // Export each transaction for (const transaction of transactions) { const transactionData = transaction; await ledgerExporter.exportTransaction(transactionData as any); } // Get all journal entries for the period // Use MongoDB query syntax for date range const journalEntries = await JournalEntry.getInstances({ date: { $gte: options.dateFrom, $lte: options.dateTo } as any, // SmartData supports MongoDB query operators skrType: this.currentSKRType }); // Export each journal entry for (const entry of journalEntries) { const entryData = entry; await ledgerExporter.exportJournalEntry(entryData as any); } const entryCount = await ledgerExporter.close(); this.logger.log('info', `Exported ${entryCount} ledger entries`); } /** * Export account data in CSV format */ private async exportAccountData(exporter: SkrExport, options: IExportOptions): Promise { const accountsExporter = new AccountsExporter(options.exportPath); // Get all accounts const accounts = await this.chartOfAccounts.getAllAccounts(); // Add each account to export for (const account of accounts) { const accountData = account; accountsExporter.addAccount(accountData as any); } // Export to CSV and JSON await accountsExporter.exportToCSV(); await accountsExporter.exportToJSON(); this.logger.log('info', `Exported ${accountsExporter.getAccountCount()} accounts`); } /** * Export balance data in CSV format */ private async exportBalanceData(exporter: SkrExport, options: IExportOptions): Promise { if (!this.ledger) throw new Error('Ledger not initialized'); const balancesExporter = new BalancesExporter( options.exportPath, options.fiscalYear ); // Get all accounts with balances const accounts = await this.chartOfAccounts.getAllAccounts(); for (const account of accounts) { const balance = await this.ledger.getAccountBalance( account.accountNumber, options.dateTo ); if (balance) { balancesExporter.addBalance( account.accountNumber, account.accountName, balance as IAccountBalance, `${options.fiscalYear}` ); } } // Export balance reports await balancesExporter.exportToCSV(); await balancesExporter.exportTrialBalance(); await balancesExporter.exportClassSummary(); this.logger.log('info', `Exported ${balancesExporter.getBalanceCount()} account balances`); } /** * Generate PDF reports for the export */ private async generatePdfReports(exporter: SkrExport, options: IExportOptions): Promise { if (!this.reports) throw new Error('Reports not initialized'); const pdfOptions: IPdfReportOptions = { companyName: options.companyInfo?.name || 'Unternehmen', companyAddress: options.companyInfo?.address, taxId: options.companyInfo?.taxId, registrationNumber: options.companyInfo?.registrationNumber, fiscalYear: options.fiscalYear, dateFrom: options.dateFrom, dateTo: options.dateTo, preparedDate: new Date() }; const pdfGenerator = new PdfReportGenerator(options.exportPath, pdfOptions); await pdfGenerator.initialize(); try { // Generate reports const trialBalance = await this.reports.getTrialBalance({ dateFrom: options.dateFrom, dateTo: options.dateTo, skrType: this.currentSKRType }); const incomeStatement = await this.reports.getIncomeStatement({ dateFrom: options.dateFrom, dateTo: options.dateTo, skrType: this.currentSKRType }); const balanceSheet = await this.reports.getBalanceSheet({ dateFrom: options.dateFrom, dateTo: options.dateTo, skrType: this.currentSKRType }); // Generate PDFs const jahresabschlussPdf = await pdfGenerator.generateJahresabschlussPdf( trialBalance, incomeStatement, balanceSheet ); // Save PDFs await pdfGenerator.savePdfReport('jahresabschluss.pdf', jahresabschlussPdf); // Store in BagIt structure await exporter.storeDocument(jahresabschlussPdf, 'jahresabschluss.pdf'); this.logger.log('info', 'PDF reports generated successfully'); } finally { await pdfGenerator.close(); } } /** * Sign the export with CAdES signature */ private async signExport(exporter: SkrExport, options: IExportOptions): Promise { const signingOptions: ISigningOptions = { certificatePem: options.signExport ? undefined : undefined, // Use provided cert or generate privateKeyPem: options.signExport ? undefined : undefined, includeTimestamp: options.timestampExport !== false }; const security = new SecurityManager(signingOptions); // Generate self-signed certificate if none provided let cert: string, key: string; if (!signingOptions.certificatePem) { const generated = await security.generateSelfSignedCertificate( options.companyInfo?.name || 'SKR Export System' ); cert = generated.certificate; key = generated.privateKey; } else { cert = signingOptions.certificatePem; key = signingOptions.privateKeyPem!; } // Sign the manifest const manifestPath = path.resolve( options.exportPath, `jahresabschluss_${options.fiscalYear}`, 'manifest-sha256.txt' ); await security.createDetachedSignature( manifestPath, path.resolve( options.exportPath, `jahresabschluss_${options.fiscalYear}`, 'data', 'metadata', 'signatures', 'manifest.cades' ) ); this.logger.log('info', 'Export signed with CAdES signature'); } // ========== 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 { 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 { 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 { 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 { 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, }; } // ========== Invoice Management ========== /** * Import an invoice from file or buffer * Parses, validates, and optionally books the invoice */ public async importInvoice( file: Buffer | string, direction: TInvoiceDirection, options?: IInvoiceImportOptions ): Promise { this.ensureInitialized(); if (!this.invoiceAdapter || !this.invoiceStorage || !this.invoiceBookingEngine) { throw new Error('Invoice components not initialized'); } this.logger.log('info', `Importing ${direction} invoice`); // Parse and validate invoice const invoice = await this.invoiceAdapter.parseInvoice(file, direction); // Store invoice await this.invoiceStorage.initialize(); const contentHash = await this.invoiceStorage.storeInvoice(invoice); invoice.contentHash = contentHash; // Auto-book if requested if (options?.autoBook) { const bookingResult = await this.bookInvoice( invoice, options.bookingRules, { autoBook: true, confidenceThreshold: options.confidenceThreshold || 80, skipValidation: options.validateOnly } ); if (bookingResult.success && bookingResult.bookingInfo) { invoice.bookingInfo = bookingResult.bookingInfo; invoice.status = 'posted'; // Update stored metadata with booking information await this.invoiceStorage.updateMetadata(invoice.contentHash, { journalEntryId: bookingResult.bookingInfo.journalEntryId, transactionIds: bookingResult.bookingInfo.transactionIds }); } } this.logger.log('info', `Invoice imported successfully: ${invoice.invoiceNumber}`); return invoice; } /** * Book an invoice to the ledger */ public async bookInvoice( invoice: IInvoice, bookingRules?: Partial, options?: IBookingOptions ): Promise { this.ensureInitialized(); if (!this.invoiceBookingEngine) { throw new Error('Invoice booking engine not initialized'); } this.logger.log('info', `Booking invoice ${invoice.invoiceNumber}`); const result = await this.invoiceBookingEngine.bookInvoice( invoice, bookingRules, options ); if (result.success) { this.logger.log('info', `Invoice booked successfully with confidence ${result.confidence}%`); // Update stored metadata if invoice has a content hash if (invoice.contentHash && result.bookingInfo && this.invoiceStorage) { await this.invoiceStorage.updateMetadata(invoice.contentHash, { journalEntryId: result.bookingInfo.journalEntryId, transactionIds: result.bookingInfo.transactionIds }); } } else { this.logger.log('error', `Invoice booking failed: ${result.errors?.join(', ')}`); } return result; } /** * Export an invoice in a different format */ public async exportInvoice( invoice: IInvoice, options: IInvoiceExportOptions ): Promise<{ xml: string; pdf?: Buffer }> { this.ensureInitialized(); if (!this.invoiceAdapter) { throw new Error('Invoice adapter not initialized'); } this.logger.log('info', `Exporting invoice ${invoice.invoiceNumber} to ${options.format}`); // Convert format if needed const xml = await this.invoiceAdapter.convertFormat(invoice, options.format); // Generate PDF if requested let pdf: Buffer | undefined; if (options.embedInPdf) { const result = await this.invoiceAdapter.generateInvoice(invoice, options.format); pdf = result.pdf; } return { xml, pdf }; } /** * Search invoices by filter */ public async searchInvoices(filter: IInvoiceFilter): Promise { this.ensureInitialized(); if (!this.invoiceStorage) { throw new Error('Invoice storage not initialized'); } await this.invoiceStorage.initialize(); const metadata = await this.invoiceStorage.searchInvoices(filter); const invoices: IInvoice[] = []; for (const meta of metadata) { const invoice = await this.invoiceStorage.retrieveInvoice(meta.contentHash); if (invoice) { invoices.push(invoice); } } return invoices; } /** * Get invoice by content hash */ public async getInvoice(contentHash: string): Promise { this.ensureInitialized(); if (!this.invoiceStorage) { throw new Error('Invoice storage not initialized'); } await this.invoiceStorage.initialize(); return await this.invoiceStorage.retrieveInvoice(contentHash); } /** * Get invoice storage statistics */ public async getInvoiceStatistics(): Promise { this.ensureInitialized(); if (!this.invoiceStorage) { throw new Error('Invoice storage not initialized'); } await this.invoiceStorage.initialize(); return await this.invoiceStorage.getStatistics(); } /** * Create EN16931 compliance report for invoices */ public async createInvoiceComplianceReport(): Promise { this.ensureInitialized(); if (!this.invoiceStorage) { throw new Error('Invoice storage not initialized'); } await this.invoiceStorage.initialize(); await this.invoiceStorage.createComplianceReport(); this.logger.log('info', 'Invoice compliance report created'); } /** * Generate an invoice from internal data */ public async generateInvoice( invoiceData: Partial, format: IInvoiceExportOptions['format'] ): Promise<{ xml: string; pdf?: Buffer }> { this.ensureInitialized(); if (!this.invoiceAdapter) { throw new Error('Invoice adapter not initialized'); } this.logger.log('info', `Generating invoice in ${format} format`); return await this.invoiceAdapter.generateInvoice(invoiceData, format); } }