1021 lines
29 KiB
TypeScript
1021 lines
29 KiB
TypeScript
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<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);
|
|
|
|
// 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<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();
|
|
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<JournalEntry> {
|
|
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<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();
|
|
}
|
|
|
|
/**
|
|
* Export Jahresabschluss in GoBD-compliant BagIt format
|
|
* Creates a revision-safe export for 10-year archival
|
|
*/
|
|
public async exportJahresabschluss(options: IExportOptions): Promise<string> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<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,
|
|
};
|
|
}
|
|
|
|
// ========== 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<IInvoice> {
|
|
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<IBookingRules>,
|
|
options?: IBookingOptions
|
|
): Promise<IBookingResult> {
|
|
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<IInvoice[]> {
|
|
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<IInvoice | null> {
|
|
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<any> {
|
|
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<void> {
|
|
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<IInvoice>,
|
|
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);
|
|
}
|
|
}
|