import * as plugins from './plugins.js'; import * as path from 'path'; import type { ITransactionData, IJournalEntry, IJournalEntryLine } from './skr.types.js'; import { createWriteStream, type WriteStream } from 'fs'; // Extended interfaces for export with additional tracking fields export interface ITransactionDataExport extends ITransactionData { _id?: string; postingDate?: Date; currency?: string; createdAt?: Date | string; modifiedAt?: Date | string; reversalOf?: string; reversedBy?: string; taxCode?: string; project?: string; vatAccount?: string; } export interface IJournalEntryExport extends IJournalEntry { _id?: string; postingDate?: Date; currency?: string; journal?: string; createdAt?: Date | string; modifiedAt?: Date | string; reversalOf?: string; reversedBy?: string; } export interface IJournalEntryLineExport extends IJournalEntryLine { taxCode?: string; project?: string; } export interface ILedgerEntry { schema_version: string; entry_id: string; booking_date: string; posting_date: string; period?: string; currency: string; journal: string; description: string; reference?: string; lines: ILedgerLine[]; document_refs?: IDocumentRef[]; created_at: string; modified_at?: string; user?: string; reversal_of?: string; reversed_by?: string; } export interface ILedgerLine { posting_id: string; account_code: string; debit: string; credit: string; tax_code?: string; cost_center?: string; project?: string; description?: string; } export interface IDocumentRef { content_hash: string; doc_role: 'invoice' | 'receipt' | 'contract' | 'bank-statement' | 'other'; doc_mime: string; doc_original_name?: string; } export class LedgerExporter { private exportPath: string; private stream: WriteStream | null = null; private entryCount: number = 0; constructor(exportPath: string) { this.exportPath = exportPath; } /** * Initializes the NDJSON export stream */ public async initialize(): Promise { const ledgerPath = path.join(this.exportPath, 'data', 'accounting', 'ledger.ndjson'); await plugins.smartfile.fs.ensureDir(path.dirname(ledgerPath)); this.stream = createWriteStream(ledgerPath, { encoding: 'utf8', flags: 'w' }); } /** * Exports a transaction as a ledger entry */ public async exportTransaction(transaction: ITransactionDataExport): Promise { if (!this.stream) { throw new Error('Ledger exporter not initialized'); } const entry: ILedgerEntry = { schema_version: '1.0', entry_id: transaction._id || plugins.smartunique.shortId(), booking_date: this.formatDate(transaction.date), posting_date: this.formatDate(transaction.postingDate || transaction.date), currency: transaction.currency || 'EUR', journal: 'GL', description: transaction.description, reference: transaction.reference, lines: [], created_at: transaction.createdAt ? new Date(transaction.createdAt).toISOString() : new Date().toISOString(), modified_at: transaction.modifiedAt ? new Date(transaction.modifiedAt).toISOString() : undefined, reversal_of: transaction.reversalOf, reversed_by: transaction.reversedBy }; // Add debit line if (transaction.amount > 0) { entry.lines.push({ posting_id: `${entry.entry_id}-1`, account_code: transaction.debitAccount, debit: transaction.amount.toFixed(2), credit: '0.00', tax_code: transaction.taxCode, cost_center: transaction.costCenter, project: transaction.project }); // Add credit line entry.lines.push({ posting_id: `${entry.entry_id}-2`, account_code: transaction.creditAccount, debit: '0.00', credit: transaction.amount.toFixed(2) }); } // Add VAT lines if applicable if (transaction.vatAmount && transaction.vatAmount > 0) { entry.lines.push({ posting_id: `${entry.entry_id}-3`, account_code: transaction.vatAccount || '1576', // Default VAT account debit: transaction.vatAmount.toFixed(2), credit: '0.00', description: 'Vorsteuer' }); } await this.writeLine(entry); } /** * Exports a journal entry */ public async exportJournalEntry(journalEntry: IJournalEntryExport): Promise { if (!this.stream) { throw new Error('Ledger exporter not initialized'); } const entry: ILedgerEntry = { schema_version: '1.0', entry_id: journalEntry._id || plugins.smartunique.shortId(), booking_date: this.formatDate(journalEntry.date), posting_date: this.formatDate(journalEntry.postingDate || journalEntry.date), currency: journalEntry.currency || 'EUR', journal: journalEntry.journal || 'GL', description: journalEntry.description, reference: journalEntry.reference, lines: [], created_at: journalEntry.createdAt ? new Date(journalEntry.createdAt).toISOString() : new Date().toISOString(), modified_at: journalEntry.modifiedAt ? new Date(journalEntry.modifiedAt).toISOString() : undefined, reversal_of: journalEntry.reversalOf, reversed_by: journalEntry.reversedBy }; // Convert journal entry lines journalEntry.lines.forEach((line, index) => { const extLine = line as IJournalEntryLineExport; entry.lines.push({ posting_id: `${entry.entry_id}-${index + 1}`, account_code: line.accountNumber, debit: (line.debit || 0).toFixed(2), credit: (line.credit || 0).toFixed(2), tax_code: extLine.taxCode, cost_center: line.costCenter, project: extLine.project, description: line.description }); }); await this.writeLine(entry); } /** * Writes a single NDJSON line */ private async writeLine(entry: ILedgerEntry): Promise { return new Promise((resolve, reject) => { if (!this.stream) { reject(new Error('Stream not initialized')); return; } const line = JSON.stringify(entry) + '\n'; this.stream.write(line, (error) => { if (error) { reject(error); } else { this.entryCount++; resolve(); } }); }); } /** * Formats a date to ISO date string */ private formatDate(date: Date | string): string { if (typeof date === 'string') { return date.split('T')[0]; } return date.toISOString().split('T')[0]; } /** * Closes the export stream */ public async close(): Promise { return new Promise((resolve) => { if (this.stream) { this.stream.end(() => { resolve(this.entryCount); }); } else { resolve(this.entryCount); } }); } /** * Gets the number of exported entries */ public getEntryCount(): number { return this.entryCount; } }