feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
This commit is contained in:
485
ts/skr.api.ts
485
ts/skr.api.ts
@@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user