feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped

This commit is contained in:
2025-08-12 12:37:01 +00:00
parent 08d7803be2
commit 73b46f7857
19 changed files with 6211 additions and 20 deletions

View File

@@ -1,10 +1,28 @@
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,
@@ -17,6 +35,7 @@ import type {
ITrialBalanceReport,
IIncomeStatement,
IBalanceSheet,
IAccountBalance,
} from './skr.types.js';
/**
@@ -29,6 +48,9 @@ export class SkrApi {
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);
@@ -62,6 +84,13 @@ export class SkrApi {
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');
@@ -350,6 +379,262 @@ export class SkrApi {
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 ==========
/**
@@ -532,4 +817,204 @@ export class SkrApi {
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);
}
}