@fin.cx/skr 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist_ts/index.d.ts +6 -0
  2. package/dist_ts/index.js +7 -1
  3. package/dist_ts/plugins.d.ts +8 -1
  4. package/dist_ts/plugins.js +10 -2
  5. package/dist_ts/skr.api.d.ts +70 -0
  6. package/dist_ts/skr.api.js +354 -3
  7. package/dist_ts/skr.classes.journalentry.d.ts +1 -0
  8. package/dist_ts/skr.classes.journalentry.js +55 -15
  9. package/dist_ts/skr.classes.ledger.d.ts +4 -0
  10. package/dist_ts/skr.classes.ledger.js +72 -1
  11. package/dist_ts/skr.classes.reports.js +56 -22
  12. package/dist_ts/skr.export.accounts.d.ts +53 -0
  13. package/dist_ts/skr.export.accounts.js +111 -0
  14. package/dist_ts/skr.export.balances.d.ts +59 -0
  15. package/dist_ts/skr.export.balances.js +205 -0
  16. package/dist_ts/skr.export.d.ts +110 -0
  17. package/dist_ts/skr.export.js +315 -0
  18. package/dist_ts/skr.export.ledger.d.ts +95 -0
  19. package/dist_ts/skr.export.ledger.js +164 -0
  20. package/dist_ts/skr.export.pdf.d.ts +82 -0
  21. package/dist_ts/skr.export.pdf.js +548 -0
  22. package/dist_ts/skr.invoice.adapter.d.ts +98 -0
  23. package/dist_ts/skr.invoice.adapter.js +476 -0
  24. package/dist_ts/skr.invoice.booking.d.ts +102 -0
  25. package/dist_ts/skr.invoice.booking.js +556 -0
  26. package/dist_ts/skr.invoice.entity.d.ts +287 -0
  27. package/dist_ts/skr.invoice.entity.js +2 -0
  28. package/dist_ts/skr.invoice.mapper.d.ts +69 -0
  29. package/dist_ts/skr.invoice.mapper.js +401 -0
  30. package/dist_ts/skr.invoice.storage.d.ts +140 -0
  31. package/dist_ts/skr.invoice.storage.js +529 -0
  32. package/dist_ts/skr.security.d.ts +65 -0
  33. package/dist_ts/skr.security.js +319 -0
  34. package/dist_ts/skr.types.d.ts +1 -0
  35. package/package.json +18 -12
  36. package/readme.md +461 -132
  37. package/ts/index.ts +6 -0
  38. package/ts/plugins.ts +22 -1
  39. package/ts/skr.api.ts +489 -2
  40. package/ts/skr.classes.journalentry.ts +63 -14
  41. package/ts/skr.classes.ledger.ts +85 -0
  42. package/ts/skr.classes.reports.ts +64 -21
  43. package/ts/skr.export.accounts.ts +154 -0
  44. package/ts/skr.export.balances.ts +270 -0
  45. package/ts/skr.export.ledger.ts +249 -0
  46. package/ts/skr.export.pdf.ts +601 -0
  47. package/ts/skr.export.ts +443 -0
  48. package/ts/skr.invoice.adapter.ts +581 -0
  49. package/ts/skr.invoice.booking.ts +738 -0
  50. package/ts/skr.invoice.entity.ts +351 -0
  51. package/ts/skr.invoice.mapper.ts +486 -0
  52. package/ts/skr.invoice.storage.ts +710 -0
  53. package/ts/skr.security.ts +405 -0
  54. package/ts/skr.types.ts +1 -0
package/ts/index.ts CHANGED
@@ -8,3 +8,9 @@ export * from './skr.classes.reports.js';
8
8
  export * from './skr.api.js';
9
9
  export * from './skr03.data.js';
10
10
  export * from './skr04.data.js';
11
+ export * from './skr.export.js';
12
+ export * from './skr.export.ledger.js';
13
+ export * from './skr.export.accounts.js';
14
+ export * from './skr.export.balances.js';
15
+ export * from './skr.export.pdf.js';
16
+ export * from './skr.security.js';
package/ts/plugins.ts CHANGED
@@ -3,5 +3,26 @@ import * as smartdata from '@push.rocks/smartdata';
3
3
  import * as smartunique from '@push.rocks/smartunique';
4
4
  import * as smarttime from '@push.rocks/smarttime';
5
5
  import * as smartlog from '@push.rocks/smartlog';
6
+ import * as smartfile from '@push.rocks/smartfile';
7
+ import * as smarthash from '@push.rocks/smarthash';
8
+ import * as smartpath from '@push.rocks/smartpath';
9
+ import * as smartpdf from '@push.rocks/smartpdf';
6
10
 
7
- export { smartdata, smartunique, smarttime, smartlog };
11
+ // third party
12
+ import * as nodeForge from 'node-forge';
13
+ import { MerkleTree } from 'merkletreejs';
14
+ import * as einvoice from '@fin.cx/einvoice';
15
+
16
+ export {
17
+ smartdata,
18
+ smartunique,
19
+ smarttime,
20
+ smartlog,
21
+ smartfile,
22
+ smarthash,
23
+ smartpath,
24
+ smartpdf,
25
+ nodeForge,
26
+ MerkleTree,
27
+ einvoice
28
+ };
package/ts/skr.api.ts CHANGED
@@ -1,10 +1,28 @@
1
1
  import * as plugins from './plugins.js';
2
+ import * as path from 'path';
2
3
  import { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
3
4
  import { Ledger } from './skr.classes.ledger.js';
4
5
  import { Reports } from './skr.classes.reports.js';
5
6
  import { Account } from './skr.classes.account.js';
6
7
  import { Transaction } from './skr.classes.transaction.js';
7
8
  import { JournalEntry } from './skr.classes.journalentry.js';
9
+ import { SkrExport, type IExportOptions } from './skr.export.js';
10
+ import { LedgerExporter } from './skr.export.ledger.js';
11
+ import { AccountsExporter } from './skr.export.accounts.js';
12
+ import { BalancesExporter } from './skr.export.balances.js';
13
+ import { PdfReportGenerator, type IPdfReportOptions } from './skr.export.pdf.js';
14
+ import { SecurityManager, type ISigningOptions } from './skr.security.js';
15
+ import { InvoiceAdapter } from './skr.invoice.adapter.js';
16
+ import { InvoiceStorage } from './skr.invoice.storage.js';
17
+ import { InvoiceBookingEngine, type IBookingOptions, type IBookingResult } from './skr.invoice.booking.js';
18
+ import type {
19
+ IInvoice,
20
+ IInvoiceFilter,
21
+ IInvoiceImportOptions,
22
+ IInvoiceExportOptions,
23
+ IBookingRules,
24
+ TInvoiceDirection,
25
+ } from './skr.invoice.entity.js';
8
26
  import type {
9
27
  IDatabaseConfig,
10
28
  TSKRType,
@@ -17,6 +35,7 @@ import type {
17
35
  ITrialBalanceReport,
18
36
  IIncomeStatement,
19
37
  IBalanceSheet,
38
+ IAccountBalance,
20
39
  } from './skr.types.js';
21
40
 
22
41
  /**
@@ -29,6 +48,9 @@ export class SkrApi {
29
48
  private logger: plugins.smartlog.Smartlog;
30
49
  private initialized: boolean = false;
31
50
  private currentSKRType: TSKRType | null = null;
51
+ private invoiceAdapter: InvoiceAdapter | null = null;
52
+ private invoiceStorage: InvoiceStorage | null = null;
53
+ private invoiceBookingEngine: InvoiceBookingEngine | null = null;
32
54
 
33
55
  constructor(private config: IDatabaseConfig) {
34
56
  this.chartOfAccounts = new ChartOfAccounts(config);
@@ -62,6 +84,13 @@ export class SkrApi {
62
84
  this.currentSKRType = skrType;
63
85
  this.ledger = new Ledger(skrType);
64
86
  this.reports = new Reports(skrType);
87
+
88
+ // Initialize invoice components
89
+ this.invoiceAdapter = new InvoiceAdapter();
90
+ const invoicePath = this.config.invoiceExportPath || path.resolve(process.cwd(), 'exports', 'invoices');
91
+ this.invoiceStorage = new InvoiceStorage(invoicePath);
92
+ this.invoiceBookingEngine = new InvoiceBookingEngine(skrType);
93
+
65
94
  this.initialized = true;
66
95
 
67
96
  this.logger.log('info', 'SKR API initialized successfully');
@@ -158,7 +187,8 @@ export class SkrApi {
158
187
  transactionData: ITransactionData,
159
188
  ): Promise<Transaction> {
160
189
  this.ensureInitialized();
161
- return await this.chartOfAccounts.postTransaction(transactionData);
190
+ if (!this.ledger) throw new Error('Ledger not initialized');
191
+ return await this.ledger.postTransaction(transactionData);
162
192
  }
163
193
 
164
194
  /**
@@ -168,7 +198,8 @@ export class SkrApi {
168
198
  journalData: IJournalEntry,
169
199
  ): Promise<JournalEntry> {
170
200
  this.ensureInitialized();
171
- return await this.chartOfAccounts.postJournalEntry(journalData);
201
+ if (!this.ledger) throw new Error('Ledger not initialized');
202
+ return await this.ledger.postJournalEntry(journalData);
172
203
  }
173
204
 
174
205
  /**
@@ -348,6 +379,262 @@ export class SkrApi {
348
379
  return await this.chartOfAccounts.exportAccountsToCSV();
349
380
  }
350
381
 
382
+ /**
383
+ * Export Jahresabschluss in GoBD-compliant BagIt format
384
+ * Creates a revision-safe export for 10-year archival
385
+ */
386
+ public async exportJahresabschluss(options: IExportOptions): Promise<string> {
387
+ this.ensureInitialized();
388
+ if (!this.ledger || !this.reports || !this.currentSKRType) {
389
+ throw new Error('API not fully initialized');
390
+ }
391
+
392
+ this.logger.log('info', `Starting Jahresabschluss export for fiscal year ${options.fiscalYear}`);
393
+
394
+ // Create export instance
395
+ const exporter = new SkrExport(options);
396
+
397
+ // Create BagIt structure
398
+ await exporter.createBagItStructure();
399
+ await exporter.createExportMetadata(this.currentSKRType);
400
+ await exporter.createSchemas();
401
+
402
+ // Export accounting data
403
+ await this.exportLedgerData(exporter, options);
404
+ await this.exportAccountData(exporter, options);
405
+ await this.exportBalanceData(exporter, options);
406
+
407
+ // Generate PDF reports if requested
408
+ if (options.generatePdfReports) {
409
+ await this.generatePdfReports(exporter, options);
410
+ }
411
+
412
+ // Sign export if requested
413
+ if (options.signExport) {
414
+ await this.signExport(exporter, options);
415
+ }
416
+
417
+ // Create manifests and validate
418
+ await exporter.writeManifests();
419
+ const merkleRoot = await exporter.createMerkleTree();
420
+
421
+ const isValid = await exporter.validateBagIt();
422
+ if (!isValid) {
423
+ throw new Error('BagIt validation failed');
424
+ }
425
+
426
+ this.logger.log('ok', `Jahresabschluss export completed. Merkle root: ${merkleRoot}`);
427
+
428
+ return options.exportPath;
429
+ }
430
+
431
+ /**
432
+ * Export ledger data in NDJSON format
433
+ */
434
+ private async exportLedgerData(exporter: SkrExport, options: IExportOptions): Promise<void> {
435
+ if (!this.ledger) throw new Error('Ledger not initialized');
436
+
437
+ const ledgerExporter = new LedgerExporter(options.exportPath);
438
+ await ledgerExporter.initialize();
439
+
440
+ // Get all transactions for the period
441
+ const transactions = await this.chartOfAccounts.getTransactions({
442
+ dateFrom: options.dateFrom,
443
+ dateTo: options.dateTo
444
+ });
445
+
446
+ // Export each transaction
447
+ for (const transaction of transactions) {
448
+ const transactionData = transaction;
449
+ await ledgerExporter.exportTransaction(transactionData as any);
450
+ }
451
+
452
+ // Get all journal entries for the period
453
+ // Use MongoDB query syntax for date range
454
+ const journalEntries = await JournalEntry.getInstances({
455
+ date: {
456
+ $gte: options.dateFrom,
457
+ $lte: options.dateTo
458
+ } as any, // SmartData supports MongoDB query operators
459
+ skrType: this.currentSKRType
460
+ });
461
+
462
+ // Export each journal entry
463
+ for (const entry of journalEntries) {
464
+ const entryData = entry;
465
+ await ledgerExporter.exportJournalEntry(entryData as any);
466
+ }
467
+
468
+ const entryCount = await ledgerExporter.close();
469
+ this.logger.log('info', `Exported ${entryCount} ledger entries`);
470
+ }
471
+
472
+ /**
473
+ * Export account data in CSV format
474
+ */
475
+ private async exportAccountData(exporter: SkrExport, options: IExportOptions): Promise<void> {
476
+ const accountsExporter = new AccountsExporter(options.exportPath);
477
+
478
+ // Get all accounts
479
+ const accounts = await this.chartOfAccounts.getAllAccounts();
480
+
481
+ // Add each account to export
482
+ for (const account of accounts) {
483
+ const accountData = account;
484
+ accountsExporter.addAccount(accountData as any);
485
+ }
486
+
487
+ // Export to CSV and JSON
488
+ await accountsExporter.exportToCSV();
489
+ await accountsExporter.exportToJSON();
490
+
491
+ this.logger.log('info', `Exported ${accountsExporter.getAccountCount()} accounts`);
492
+ }
493
+
494
+ /**
495
+ * Export balance data in CSV format
496
+ */
497
+ private async exportBalanceData(exporter: SkrExport, options: IExportOptions): Promise<void> {
498
+ if (!this.ledger) throw new Error('Ledger not initialized');
499
+
500
+ const balancesExporter = new BalancesExporter(
501
+ options.exportPath,
502
+ options.fiscalYear
503
+ );
504
+
505
+ // Get all accounts with balances
506
+ const accounts = await this.chartOfAccounts.getAllAccounts();
507
+
508
+ for (const account of accounts) {
509
+ const balance = await this.ledger.getAccountBalance(
510
+ account.accountNumber,
511
+ options.dateTo
512
+ );
513
+
514
+ if (balance) {
515
+ balancesExporter.addBalance(
516
+ account.accountNumber,
517
+ account.accountName,
518
+ balance as IAccountBalance,
519
+ `${options.fiscalYear}`
520
+ );
521
+ }
522
+ }
523
+
524
+ // Export balance reports
525
+ await balancesExporter.exportToCSV();
526
+ await balancesExporter.exportTrialBalance();
527
+ await balancesExporter.exportClassSummary();
528
+
529
+ this.logger.log('info', `Exported ${balancesExporter.getBalanceCount()} account balances`);
530
+ }
531
+
532
+ /**
533
+ * Generate PDF reports for the export
534
+ */
535
+ private async generatePdfReports(exporter: SkrExport, options: IExportOptions): Promise<void> {
536
+ if (!this.reports) throw new Error('Reports not initialized');
537
+
538
+ const pdfOptions: IPdfReportOptions = {
539
+ companyName: options.companyInfo?.name || 'Unternehmen',
540
+ companyAddress: options.companyInfo?.address,
541
+ taxId: options.companyInfo?.taxId,
542
+ registrationNumber: options.companyInfo?.registrationNumber,
543
+ fiscalYear: options.fiscalYear,
544
+ dateFrom: options.dateFrom,
545
+ dateTo: options.dateTo,
546
+ preparedDate: new Date()
547
+ };
548
+
549
+ const pdfGenerator = new PdfReportGenerator(options.exportPath, pdfOptions);
550
+ await pdfGenerator.initialize();
551
+
552
+ try {
553
+ // Generate reports
554
+ const trialBalance = await this.reports.getTrialBalance({
555
+ dateFrom: options.dateFrom,
556
+ dateTo: options.dateTo,
557
+ skrType: this.currentSKRType
558
+ });
559
+
560
+ const incomeStatement = await this.reports.getIncomeStatement({
561
+ dateFrom: options.dateFrom,
562
+ dateTo: options.dateTo,
563
+ skrType: this.currentSKRType
564
+ });
565
+
566
+ const balanceSheet = await this.reports.getBalanceSheet({
567
+ dateFrom: options.dateFrom,
568
+ dateTo: options.dateTo,
569
+ skrType: this.currentSKRType
570
+ });
571
+
572
+ // Generate PDFs
573
+ const jahresabschlussPdf = await pdfGenerator.generateJahresabschlussPdf(
574
+ trialBalance,
575
+ incomeStatement,
576
+ balanceSheet
577
+ );
578
+
579
+ // Save PDFs
580
+ await pdfGenerator.savePdfReport('jahresabschluss.pdf', jahresabschlussPdf);
581
+
582
+ // Store in BagIt structure
583
+ await exporter.storeDocument(jahresabschlussPdf, 'jahresabschluss.pdf');
584
+
585
+ this.logger.log('info', 'PDF reports generated successfully');
586
+ } finally {
587
+ await pdfGenerator.close();
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Sign the export with CAdES signature
593
+ */
594
+ private async signExport(exporter: SkrExport, options: IExportOptions): Promise<void> {
595
+ const signingOptions: ISigningOptions = {
596
+ certificatePem: options.signExport ? undefined : undefined, // Use provided cert or generate
597
+ privateKeyPem: options.signExport ? undefined : undefined,
598
+ includeTimestamp: options.timestampExport !== false
599
+ };
600
+
601
+ const security = new SecurityManager(signingOptions);
602
+
603
+ // Generate self-signed certificate if none provided
604
+ let cert: string, key: string;
605
+ if (!signingOptions.certificatePem) {
606
+ const generated = await security.generateSelfSignedCertificate(
607
+ options.companyInfo?.name || 'SKR Export System'
608
+ );
609
+ cert = generated.certificate;
610
+ key = generated.privateKey;
611
+ } else {
612
+ cert = signingOptions.certificatePem;
613
+ key = signingOptions.privateKeyPem!;
614
+ }
615
+
616
+ // Sign the manifest
617
+ const manifestPath = path.resolve(
618
+ options.exportPath,
619
+ `jahresabschluss_${options.fiscalYear}`,
620
+ 'manifest-sha256.txt'
621
+ );
622
+
623
+ await security.createDetachedSignature(
624
+ manifestPath,
625
+ path.resolve(
626
+ options.exportPath,
627
+ `jahresabschluss_${options.fiscalYear}`,
628
+ 'data',
629
+ 'metadata',
630
+ 'signatures',
631
+ 'manifest.cades'
632
+ )
633
+ );
634
+
635
+ this.logger.log('info', 'Export signed with CAdES signature');
636
+ }
637
+
351
638
  // ========== Utility Methods ==========
352
639
 
353
640
  /**
@@ -530,4 +817,204 @@ export class SkrApi {
530
817
  totalPages,
531
818
  };
532
819
  }
820
+
821
+ // ========== Invoice Management ==========
822
+
823
+ /**
824
+ * Import an invoice from file or buffer
825
+ * Parses, validates, and optionally books the invoice
826
+ */
827
+ public async importInvoice(
828
+ file: Buffer | string,
829
+ direction: TInvoiceDirection,
830
+ options?: IInvoiceImportOptions
831
+ ): Promise<IInvoice> {
832
+ this.ensureInitialized();
833
+ if (!this.invoiceAdapter || !this.invoiceStorage || !this.invoiceBookingEngine) {
834
+ throw new Error('Invoice components not initialized');
835
+ }
836
+
837
+ this.logger.log('info', `Importing ${direction} invoice`);
838
+
839
+ // Parse and validate invoice
840
+ const invoice = await this.invoiceAdapter.parseInvoice(file, direction);
841
+
842
+ // Store invoice
843
+ await this.invoiceStorage.initialize();
844
+ const contentHash = await this.invoiceStorage.storeInvoice(invoice);
845
+ invoice.contentHash = contentHash;
846
+
847
+ // Auto-book if requested
848
+ if (options?.autoBook) {
849
+ const bookingResult = await this.bookInvoice(
850
+ invoice,
851
+ options.bookingRules,
852
+ {
853
+ autoBook: true,
854
+ confidenceThreshold: options.confidenceThreshold || 80,
855
+ skipValidation: options.validateOnly
856
+ }
857
+ );
858
+
859
+ if (bookingResult.success && bookingResult.bookingInfo) {
860
+ invoice.bookingInfo = bookingResult.bookingInfo;
861
+ invoice.status = 'posted';
862
+
863
+ // Update stored metadata with booking information
864
+ await this.invoiceStorage.updateMetadata(invoice.contentHash, {
865
+ journalEntryId: bookingResult.bookingInfo.journalEntryId,
866
+ transactionIds: bookingResult.bookingInfo.transactionIds
867
+ });
868
+ }
869
+ }
870
+
871
+ this.logger.log('info', `Invoice imported successfully: ${invoice.invoiceNumber}`);
872
+ return invoice;
873
+ }
874
+
875
+ /**
876
+ * Book an invoice to the ledger
877
+ */
878
+ public async bookInvoice(
879
+ invoice: IInvoice,
880
+ bookingRules?: Partial<IBookingRules>,
881
+ options?: IBookingOptions
882
+ ): Promise<IBookingResult> {
883
+ this.ensureInitialized();
884
+ if (!this.invoiceBookingEngine) {
885
+ throw new Error('Invoice booking engine not initialized');
886
+ }
887
+
888
+ this.logger.log('info', `Booking invoice ${invoice.invoiceNumber}`);
889
+
890
+ const result = await this.invoiceBookingEngine.bookInvoice(
891
+ invoice,
892
+ bookingRules,
893
+ options
894
+ );
895
+
896
+ if (result.success) {
897
+ this.logger.log('info', `Invoice booked successfully with confidence ${result.confidence}%`);
898
+
899
+ // Update stored metadata if invoice has a content hash
900
+ if (invoice.contentHash && result.bookingInfo && this.invoiceStorage) {
901
+ await this.invoiceStorage.updateMetadata(invoice.contentHash, {
902
+ journalEntryId: result.bookingInfo.journalEntryId,
903
+ transactionIds: result.bookingInfo.transactionIds
904
+ });
905
+ }
906
+ } else {
907
+ this.logger.log('error', `Invoice booking failed: ${result.errors?.join(', ')}`);
908
+ }
909
+
910
+ return result;
911
+ }
912
+
913
+ /**
914
+ * Export an invoice in a different format
915
+ */
916
+ public async exportInvoice(
917
+ invoice: IInvoice,
918
+ options: IInvoiceExportOptions
919
+ ): Promise<{ xml: string; pdf?: Buffer }> {
920
+ this.ensureInitialized();
921
+ if (!this.invoiceAdapter) {
922
+ throw new Error('Invoice adapter not initialized');
923
+ }
924
+
925
+ this.logger.log('info', `Exporting invoice ${invoice.invoiceNumber} to ${options.format}`);
926
+
927
+ // Convert format if needed
928
+ const xml = await this.invoiceAdapter.convertFormat(invoice, options.format);
929
+
930
+ // Generate PDF if requested
931
+ let pdf: Buffer | undefined;
932
+ if (options.embedInPdf) {
933
+ const result = await this.invoiceAdapter.generateInvoice(invoice, options.format);
934
+ pdf = result.pdf;
935
+ }
936
+
937
+ return { xml, pdf };
938
+ }
939
+
940
+ /**
941
+ * Search invoices by filter
942
+ */
943
+ public async searchInvoices(filter: IInvoiceFilter): Promise<IInvoice[]> {
944
+ this.ensureInitialized();
945
+ if (!this.invoiceStorage) {
946
+ throw new Error('Invoice storage not initialized');
947
+ }
948
+
949
+ await this.invoiceStorage.initialize();
950
+ const metadata = await this.invoiceStorage.searchInvoices(filter);
951
+
952
+ const invoices: IInvoice[] = [];
953
+ for (const meta of metadata) {
954
+ const invoice = await this.invoiceStorage.retrieveInvoice(meta.contentHash);
955
+ if (invoice) {
956
+ invoices.push(invoice);
957
+ }
958
+ }
959
+
960
+ return invoices;
961
+ }
962
+
963
+ /**
964
+ * Get invoice by content hash
965
+ */
966
+ public async getInvoice(contentHash: string): Promise<IInvoice | null> {
967
+ this.ensureInitialized();
968
+ if (!this.invoiceStorage) {
969
+ throw new Error('Invoice storage not initialized');
970
+ }
971
+
972
+ await this.invoiceStorage.initialize();
973
+ return await this.invoiceStorage.retrieveInvoice(contentHash);
974
+ }
975
+
976
+ /**
977
+ * Get invoice storage statistics
978
+ */
979
+ public async getInvoiceStatistics(): Promise<any> {
980
+ this.ensureInitialized();
981
+ if (!this.invoiceStorage) {
982
+ throw new Error('Invoice storage not initialized');
983
+ }
984
+
985
+ await this.invoiceStorage.initialize();
986
+ return await this.invoiceStorage.getStatistics();
987
+ }
988
+
989
+ /**
990
+ * Create EN16931 compliance report for invoices
991
+ */
992
+ public async createInvoiceComplianceReport(): Promise<void> {
993
+ this.ensureInitialized();
994
+ if (!this.invoiceStorage) {
995
+ throw new Error('Invoice storage not initialized');
996
+ }
997
+
998
+ await this.invoiceStorage.initialize();
999
+ await this.invoiceStorage.createComplianceReport();
1000
+
1001
+ this.logger.log('info', 'Invoice compliance report created');
1002
+ }
1003
+
1004
+ /**
1005
+ * Generate an invoice from internal data
1006
+ */
1007
+ public async generateInvoice(
1008
+ invoiceData: Partial<IInvoice>,
1009
+ format: IInvoiceExportOptions['format']
1010
+ ): Promise<{ xml: string; pdf?: Buffer }> {
1011
+ this.ensureInitialized();
1012
+ if (!this.invoiceAdapter) {
1013
+ throw new Error('Invoice adapter not initialized');
1014
+ }
1015
+
1016
+ this.logger.log('info', `Generating invoice in ${format} format`);
1017
+
1018
+ return await this.invoiceAdapter.generateInvoice(invoiceData, format);
1019
+ }
533
1020
  }
@@ -96,6 +96,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
96
96
  this.postedAt = null;
97
97
  this.createdBy = 'system';
98
98
 
99
+ // Normalize any negative amounts to the correct side
100
+ this.sanitizeLines();
99
101
  // Calculate totals
100
102
  this.calculateTotals();
101
103
  }
@@ -107,6 +109,36 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
107
109
  return `JE-${timestamp}-${random}`;
108
110
  }
109
111
 
112
+ private sanitizeLines(): void {
113
+ for (const line of this.lines) {
114
+ // Check if both debit and credit are set (not allowed)
115
+ if (line.debit !== undefined && line.debit !== 0 &&
116
+ line.credit !== undefined && line.credit !== 0) {
117
+ throw new Error('A line cannot have both debit and credit amounts');
118
+ }
119
+
120
+ // Handle negative debit - convert to positive credit
121
+ if (line.debit !== undefined && line.debit < 0) {
122
+ line.credit = Math.abs(line.debit);
123
+ delete (line as any).debit;
124
+ }
125
+
126
+ // Handle negative credit - convert to positive debit
127
+ if (line.credit !== undefined && line.credit < 0) {
128
+ line.debit = Math.abs(line.credit);
129
+ delete (line as any).credit;
130
+ }
131
+
132
+ // Check that at least one side has a positive value
133
+ const hasDebit = line.debit !== undefined && line.debit > 0;
134
+ const hasCredit = line.credit !== undefined && line.credit > 0;
135
+
136
+ if (!hasDebit && !hasCredit) {
137
+ throw new Error('Either debit or credit must be a positive number');
138
+ }
139
+ }
140
+ }
141
+
110
142
  private calculateTotals(): void {
111
143
  this.totalDebits = 0;
112
144
  this.totalCredits = 0;
@@ -204,6 +236,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
204
236
  throw new Error('Journal entry is already posted');
205
237
  }
206
238
 
239
+ // Normalize any negative amounts to the correct side
240
+ this.sanitizeLines();
207
241
  // Validate before posting
208
242
  await this.validate();
209
243
 
@@ -230,28 +264,41 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
230
264
  transactions.push(transaction);
231
265
  } else {
232
266
  // Complex entry: multiple debits and/or credits
233
- // Create transactions to balance the entry
234
- for (const debitLine of debitLines) {
235
- for (const creditLine of creditLines) {
236
- const amount = Math.min(debitLine.debit || 0, creditLine.credit || 0);
267
+ // Build working queues with remaining amounts (don't mutate original lines)
268
+ const debitQueue = debitLines.map(l => ({
269
+ line: l,
270
+ remaining: l.debit || 0
271
+ }));
272
+
273
+ const creditQueue = creditLines.map(l => ({
274
+ line: l,
275
+ remaining: l.credit || 0
276
+ }));
237
277
 
238
- if (amount > 0) {
278
+ // Create transactions to balance the entry
279
+ for (const d of debitQueue) {
280
+ for (const c of creditQueue) {
281
+ const amount = Math.min(d.remaining, c.remaining);
282
+
283
+ if (amount > 0.0000001) { // small epsilon to avoid float artifacts
239
284
  const transaction = await Transaction.createTransaction({
240
285
  date: this.date,
241
- debitAccount: debitLine.accountNumber,
242
- creditAccount: creditLine.accountNumber,
243
- amount: amount,
244
- description: `${this.description} - ${debitLine.description || creditLine.description || ''}`,
286
+ debitAccount: d.line.accountNumber,
287
+ creditAccount: c.line.accountNumber,
288
+ amount: Math.round(amount * 100) / 100, // round to 2 decimals
289
+ description: `${this.description} - ${d.line.description || c.line.description || ''}`,
245
290
  reference: this.reference,
246
291
  skrType: this.skrType,
247
- costCenter: debitLine.costCenter || creditLine.costCenter,
292
+ costCenter: d.line.costCenter || c.line.costCenter,
248
293
  });
249
294
  transactions.push(transaction);
250
-
251
- // Reduce amounts for tracking
252
- if (debitLine.debit) debitLine.debit -= amount;
253
- if (creditLine.credit) creditLine.credit -= amount;
295
+
296
+ // Reduce remaining amounts in working copies (not original lines)
297
+ d.remaining -= amount;
298
+ c.remaining -= amount;
254
299
  }
300
+
301
+ if (d.remaining <= 0.0000001) break;
255
302
  }
256
303
  }
257
304
  }
@@ -299,6 +346,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
299
346
  }
300
347
 
301
348
  public async beforeSave(): Promise<void> {
349
+ // Normalize any negative amounts to the correct side
350
+ this.sanitizeLines();
302
351
  // Recalculate totals before saving
303
352
  this.calculateTotals();
304
353