@fin.cx/skr 1.3.0 → 2.0.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 (94) hide show
  1. package/.smartconfig.json +33 -6
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/core.accountpolicy.d.ts +67 -0
  4. package/dist_ts/core/core.accountpolicy.js +91 -0
  5. package/dist_ts/core/core.bukeys.d.ts +38 -0
  6. package/dist_ts/core/core.bukeys.js +117 -0
  7. package/dist_ts/core/core.datev.captions.d.ts +7 -0
  8. package/dist_ts/core/core.datev.captions.js +134 -0
  9. package/dist_ts/core/core.datev.d.ts +47 -0
  10. package/dist_ts/core/core.datev.js +129 -0
  11. package/dist_ts/core/core.hash.d.ts +27 -0
  12. package/dist_ts/core/core.hash.js +72 -0
  13. package/dist_ts/core/core.plugins.d.ts +3 -0
  14. package/dist_ts/core/core.plugins.js +9 -0
  15. package/dist_ts/core/core.recipes.d.ts +118 -0
  16. package/dist_ts/core/core.recipes.js +504 -0
  17. package/dist_ts/core/core.taxscenario.d.ts +27 -0
  18. package/dist_ts/core/core.taxscenario.js +85 -0
  19. package/dist_ts/core/core.types.d.ts +48 -0
  20. package/dist_ts/core/core.types.js +2 -0
  21. package/dist_ts/core/core.validate.d.ts +17 -0
  22. package/dist_ts/core/core.validate.js +89 -0
  23. package/dist_ts/core/index.d.ts +15 -0
  24. package/dist_ts/core/index.js +16 -0
  25. package/dist_ts/index.d.ts +15 -0
  26. package/dist_ts/index.js +10 -1
  27. package/dist_ts/plugins.d.ts +2 -1
  28. package/dist_ts/plugins.js +4 -2
  29. package/dist_ts/skr.api.d.ts +91 -22
  30. package/dist_ts/skr.api.js +357 -42
  31. package/dist_ts/skr.balances.d.ts +28 -0
  32. package/dist_ts/skr.balances.js +96 -0
  33. package/dist_ts/skr.classes.journalentry.d.ts +19 -3
  34. package/dist_ts/skr.classes.journalentry.js +67 -4
  35. package/dist_ts/skr.classes.reports.js +49 -102
  36. package/dist_ts/skr.database.d.ts +2 -0
  37. package/dist_ts/skr.database.js +1 -1
  38. package/dist_ts/skr.errors.d.ts +18 -0
  39. package/dist_ts/skr.errors.js +34 -0
  40. package/dist_ts/skr.export.js +4 -7
  41. package/dist_ts/skr.invoice.adapter.d.ts +15 -1
  42. package/dist_ts/skr.invoice.adapter.js +253 -41
  43. package/dist_ts/skr.invoice.booking.js +14 -28
  44. package/dist_ts/skr.migrate.d.ts +20 -0
  45. package/dist_ts/skr.migrate.js +223 -0
  46. package/dist_ts/skr.period.d.ts +47 -0
  47. package/dist_ts/skr.period.js +95 -0
  48. package/dist_ts/skr.posting.d.ts +74 -0
  49. package/dist_ts/skr.posting.js +209 -0
  50. package/dist_ts/skr.security.d.ts +6 -9
  51. package/dist_ts/skr.security.js +16 -89
  52. package/dist_ts/skr.sequence.d.ts +32 -0
  53. package/dist_ts/skr.sequence.js +51 -0
  54. package/dist_ts/skr.txview.d.ts +46 -0
  55. package/dist_ts/skr.txview.js +235 -0
  56. package/dist_ts/skr.types.d.ts +35 -1
  57. package/dist_ts/skr.verify.d.ts +13 -0
  58. package/dist_ts/skr.verify.js +84 -0
  59. package/package.json +21 -24
  60. package/readme.hints.md +37 -0
  61. package/readme.md +94 -1
  62. package/readme.plan.md +180 -243
  63. package/ts/00_commitinfo_data.ts +1 -1
  64. package/ts/core/core.accountpolicy.ts +154 -0
  65. package/ts/core/core.bukeys.ts +146 -0
  66. package/ts/core/core.datev.captions.ts +133 -0
  67. package/ts/core/core.datev.ts +174 -0
  68. package/ts/core/core.hash.ts +76 -0
  69. package/ts/core/core.plugins.ts +9 -0
  70. package/ts/core/core.recipes.ts +659 -0
  71. package/ts/core/core.taxscenario.ts +156 -0
  72. package/ts/core/core.types.ts +74 -0
  73. package/ts/core/core.validate.ts +119 -0
  74. package/ts/core/index.ts +15 -0
  75. package/ts/index.ts +31 -0
  76. package/ts/plugins.ts +4 -0
  77. package/ts/skr.api.ts +432 -46
  78. package/ts/skr.balances.ts +127 -0
  79. package/ts/skr.classes.journalentry.ts +54 -5
  80. package/ts/skr.classes.reports.ts +54 -126
  81. package/ts/skr.database.ts +3 -0
  82. package/ts/skr.errors.ts +44 -0
  83. package/ts/skr.export.ts +3 -6
  84. package/ts/skr.invoice.adapter.ts +291 -45
  85. package/ts/skr.invoice.booking.ts +22 -34
  86. package/ts/skr.migrate.ts +268 -0
  87. package/ts/skr.period.ts +139 -0
  88. package/ts/skr.posting.ts +273 -0
  89. package/ts/skr.security.ts +18 -93
  90. package/ts/skr.sequence.ts +89 -0
  91. package/ts/skr.txview.ts +268 -0
  92. package/ts/skr.types.ts +37 -1
  93. package/ts/skr.verify.ts +99 -0
  94. /package/{license.md → license} +0 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Balance computation for the v2 ledger: always aggregated from the
3
+ * journal, never incrementally stored. Exact integer cents throughout.
4
+ */
5
+ import { getDbSync } from './skr.database.js';
6
+ import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
7
+ import type { TSKRType } from './skr.types.js';
8
+
9
+ export interface IAccountBalanceCents {
10
+ accountNumber: string;
11
+ debitCents: number;
12
+ creditCents: number;
13
+ /** debit - credit; sign interpretation belongs to the account type */
14
+ balanceCents: number;
15
+ }
16
+
17
+ export interface ITrialBalanceCents {
18
+ accounts: IAccountBalanceCents[];
19
+ totalDebitCents: number;
20
+ totalCreditCents: number;
21
+ isBalanced: boolean;
22
+ }
23
+
24
+ export interface IBalanceQueryOptions {
25
+ dateFrom?: Date;
26
+ dateTo?: Date;
27
+ /** include festgeschriebene entries (default true) */
28
+ includeFinalized?: boolean;
29
+ }
30
+
31
+ function entryMatch(skrType: TSKRType, options?: IBalanceQueryOptions): Record<string, unknown> {
32
+ const statuses = options?.includeFinalized === false ? ['posted'] : ['posted', 'finalized'];
33
+ const match: Record<string, unknown> = {
34
+ schemaVersion: 2,
35
+ skrType,
36
+ status: { $in: statuses },
37
+ };
38
+ if (options?.dateFrom || options?.dateTo) {
39
+ const range: Record<string, Date> = {};
40
+ if (options.dateFrom) range.$gte = options.dateFrom;
41
+ if (options.dateTo) range.$lte = options.dateTo;
42
+ match.date = range;
43
+ }
44
+ return match;
45
+ }
46
+
47
+ /**
48
+ * Aggregated trial balance over all schemaVersion-2 entries.
49
+ */
50
+ export async function aggregateTrialBalance(
51
+ skrType: TSKRType,
52
+ options?: IBalanceQueryOptions,
53
+ ): Promise<ITrialBalanceCents> {
54
+ const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
55
+ const rows = await collection
56
+ .aggregate<{ _id: string; debitCents: number; creditCents: number }>([
57
+ { $match: entryMatch(skrType, options) },
58
+ { $unwind: '$lines' },
59
+ {
60
+ $group: {
61
+ _id: '$lines.accountNumber',
62
+ debitCents: {
63
+ $sum: { $cond: [{ $eq: ['$lines.side', 'debit'] }, '$lines.amountCents', 0] },
64
+ },
65
+ creditCents: {
66
+ $sum: { $cond: [{ $eq: ['$lines.side', 'credit'] }, '$lines.amountCents', 0] },
67
+ },
68
+ },
69
+ },
70
+ { $sort: { _id: 1 } },
71
+ ])
72
+ .toArray();
73
+
74
+ const accounts: IAccountBalanceCents[] = rows.map((row) => ({
75
+ accountNumber: row._id,
76
+ debitCents: row.debitCents,
77
+ creditCents: row.creditCents,
78
+ balanceCents: row.debitCents - row.creditCents,
79
+ }));
80
+ const totalDebitCents = accounts.reduce((sum, account) => sum + account.debitCents, 0);
81
+ const totalCreditCents = accounts.reduce((sum, account) => sum + account.creditCents, 0);
82
+ return {
83
+ accounts,
84
+ totalDebitCents,
85
+ totalCreditCents,
86
+ isBalanced: totalDebitCents === totalCreditCents,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Exact balance of one account.
92
+ */
93
+ export async function aggregateAccountBalance(
94
+ accountNumber: string,
95
+ skrType: TSKRType,
96
+ options?: IBalanceQueryOptions,
97
+ ): Promise<IAccountBalanceCents> {
98
+ const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
99
+ const rows = await collection
100
+ .aggregate<{ _id: string; debitCents: number; creditCents: number }>([
101
+ { $match: { ...entryMatch(skrType, options), 'lines.accountNumber': accountNumber } },
102
+ { $unwind: '$lines' },
103
+ { $match: { 'lines.accountNumber': accountNumber } },
104
+ {
105
+ $group: {
106
+ _id: '$lines.accountNumber',
107
+ debitCents: {
108
+ $sum: { $cond: [{ $eq: ['$lines.side', 'debit'] }, '$lines.amountCents', 0] },
109
+ },
110
+ creditCents: {
111
+ $sum: { $cond: [{ $eq: ['$lines.side', 'credit'] }, '$lines.amountCents', 0] },
112
+ },
113
+ },
114
+ },
115
+ ])
116
+ .toArray();
117
+ const row = rows[0];
118
+ if (!row) {
119
+ return { accountNumber, debitCents: 0, creditCents: 0, balanceCents: 0 };
120
+ }
121
+ return {
122
+ accountNumber,
123
+ debitCents: row.debitCents,
124
+ creditCents: row.creditCents,
125
+ balanceCents: row.debitCents - row.creditCents,
126
+ };
127
+ }
@@ -12,10 +12,12 @@ import type {
12
12
  IJournalEntry,
13
13
  IJournalEntryLine,
14
14
  } from './skr.types.js';
15
+ import { ImmutableEntryError } from './skr.errors.js';
15
16
 
16
17
  declare abstract class SmartDataDbDocBase {
17
- public save(): Promise<void>;
18
- public delete(): Promise<void>;
18
+ public creationStatus: 'db' | 'new' | 'mixed';
19
+ public save(opts?: { session?: any }): Promise<void>;
20
+ public delete(opts?: { session?: any }): Promise<void>;
19
21
  public static getInstance<T>(
20
22
  this: new (...args: any[]) => T,
21
23
  query: Record<string, any>,
@@ -72,7 +74,34 @@ export class JournalEntry extends SmartDataDbDoc {
72
74
 
73
75
  @svDb()
74
76
  @index()
75
- public status!: 'draft' | 'posted' | 'reversed';
77
+ public status!: 'draft' | 'posted' | 'reversed' | 'finalized';
78
+
79
+ // --- schemaVersion 2: GoBD hash chain + Festschreibung ------------------
80
+ @svDb()
81
+ public schemaVersion?: number;
82
+
83
+ @svDb()
84
+ @index()
85
+ public sequenceNumber?: number;
86
+
87
+ @svDb()
88
+ public prevHash?: string;
89
+
90
+ @svDb()
91
+ public entryHash?: string;
92
+
93
+ @svDb()
94
+ public finalizedAt?: Date | null;
95
+
96
+ @svDb()
97
+ @index()
98
+ public reversalOf?: string;
99
+
100
+ @svDb()
101
+ public taxScenario?: string;
102
+
103
+ @svDb()
104
+ public currency?: string;
76
105
 
77
106
  @svDb()
78
107
  public transactionIds!: string[];
@@ -299,8 +328,14 @@ export class JournalEntry extends SmartDataDbDoc {
299
328
  }
300
329
  }
301
330
 
302
- // Validate posting key consistency across all lines
303
- const consistencyValidation = validatePostingKeyConsistency(this.lines);
331
+ // Validate posting key consistency across all lines (v1 numeric keys only;
332
+ // schemaVersion-2 lines carry string buKeys validated by the core)
333
+ const consistencyValidation = validatePostingKeyConsistency(
334
+ this.lines.filter(
335
+ (line): line is IJournalEntryLine & { postingKey: NonNullable<IJournalEntryLine['postingKey']> } =>
336
+ line.postingKey !== undefined,
337
+ ),
338
+ );
304
339
  if (!consistencyValidation.isValid) {
305
340
  validationErrors.push(...consistencyValidation.errors);
306
341
  }
@@ -439,6 +474,13 @@ export class JournalEntry extends SmartDataDbDoc {
439
474
  }
440
475
 
441
476
  public async beforeSave(): Promise<void> {
477
+ // GoBD immutability: once a hashed (schemaVersion 2) entry is persisted it
478
+ // must never change — corrections happen via Storno entries only.
479
+ // Festschreibung stamps bypass doc saves (raw updates outside the hash).
480
+ if (this.creationStatus === 'db' && this.entryHash) {
481
+ throw new ImmutableEntryError(this.id);
482
+ }
483
+
442
484
  // Normalize any negative amounts to the correct side
443
485
  this.sanitizeLines();
444
486
  // Recalculate totals before saving
@@ -457,4 +499,11 @@ export class JournalEntry extends SmartDataDbDoc {
457
499
  throw new Error('Journal entry must have at least one line');
458
500
  }
459
501
  }
502
+
503
+ public async delete(opts?: { session?: any }): Promise<void> {
504
+ if (this.entryHash) {
505
+ throw new ImmutableEntryError(this.id);
506
+ }
507
+ await super.delete(opts);
508
+ }
460
509
  }
@@ -1,7 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { Account } from './skr.classes.account.js';
3
- import { Transaction } from './skr.classes.transaction.js';
4
3
  import { Ledger } from './skr.classes.ledger.js';
4
+ import * as core from './core/index.js';
5
+ import { accountLineViews, loadDraftsForExport } from './skr.txview.js';
5
6
  import type {
6
7
  TSKRType,
7
8
  ITrialBalanceReport,
@@ -11,6 +12,7 @@ import type {
11
12
  IBalanceSheet,
12
13
  IBalanceSheetEntry,
13
14
  IReportParams,
15
+ ITransactionView,
14
16
  } from './skr.types.js';
15
17
 
16
18
  export class Reports {
@@ -49,26 +51,37 @@ export class Reports {
49
51
  let totalCredits = 0;
50
52
 
51
53
  for (const account of accounts) {
52
- // Get balance for the period if specified
53
- const balance = params?.dateTo
54
- ? await this.ledger.getAccountBalance(
55
- account.accountNumber,
56
- params.dateTo,
57
- )
58
- : await this.ledger.getAccountBalance(account.accountNumber);
59
-
60
- if (balance.debitTotal !== 0 || balance.creditTotal !== 0) {
54
+ // Per-account activity from the v2 (hashed) journal entries
55
+ const views = await accountLineViews(
56
+ account.accountNumber,
57
+ this.skrType,
58
+ params?.dateTo
59
+ ? { skrType: this.skrType, dateTo: params.dateTo }
60
+ : undefined,
61
+ );
62
+ let debitTotal = 0;
63
+ let creditTotal = 0;
64
+ for (const view of views) {
65
+ if (view.debitAccount === account.accountNumber) debitTotal += view.amount;
66
+ if (view.creditAccount === account.accountNumber) creditTotal += view.amount;
67
+ }
68
+ const signedBalance =
69
+ account.accountType === 'asset' || account.accountType === 'expense'
70
+ ? debitTotal - creditTotal
71
+ : creditTotal - debitTotal;
72
+
73
+ if (debitTotal !== 0 || creditTotal !== 0) {
61
74
  const entry: ITrialBalanceEntry = {
62
75
  accountNumber: account.accountNumber,
63
76
  accountName: account.accountName,
64
- debitBalance: balance.debitTotal,
65
- creditBalance: balance.creditTotal,
66
- netBalance: balance.balance,
77
+ debitBalance: debitTotal,
78
+ creditBalance: creditTotal,
79
+ netBalance: signedBalance,
67
80
  };
68
81
 
69
82
  entries.push(entry);
70
- totalDebits += balance.debitTotal;
71
- totalCredits += balance.creditTotal;
83
+ totalDebits += debitTotal;
84
+ totalCredits += creditTotal;
72
85
  }
73
86
  }
74
87
 
@@ -342,48 +355,17 @@ export class Reports {
342
355
  account: Account,
343
356
  params?: IReportParams,
344
357
  ): Promise<number> {
345
- let transactions = await Transaction.getTransactionsByAccount(
346
- account.accountNumber,
347
- this.skrType,
348
- );
349
-
350
- // Apply date filter if provided
351
- if (params?.dateFrom || params?.dateTo) {
352
- // Normalize dates for inclusive comparison
353
- const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
354
- const dateTo = params.dateTo ? new Date(params.dateTo) : null;
355
-
356
- // Set dateFrom to start of day (00:00:00.000)
357
- if (dateFrom) {
358
- dateFrom.setHours(0, 0, 0, 0);
359
- }
360
-
361
- // Set dateTo to end of day (23:59:59.999) for inclusive comparison
362
- if (dateTo) {
363
- dateTo.setHours(23, 59, 59, 999);
364
- }
365
-
366
- transactions = transactions.filter((transaction) => {
367
- const txDate = transaction.date instanceof Date
368
- ? transaction.date
369
- : new Date(transaction.date);
370
- const txTime = txDate.getTime();
371
-
372
- if (dateFrom && txTime < dateFrom.getTime()) return false;
373
- if (dateTo && txTime > dateTo.getTime()) return false;
374
- return true;
375
- });
376
- }
358
+ // v2: per-account activity is projected from hashed journal entries
359
+ const views = await accountLineViews(account.accountNumber, this.skrType, params);
377
360
 
378
361
  let debitTotal = 0;
379
362
  let creditTotal = 0;
380
-
381
- for (const transaction of transactions) {
382
- if (transaction.debitAccount === account.accountNumber) {
383
- debitTotal += transaction.amount;
363
+ for (const view of views) {
364
+ if (view.debitAccount === account.accountNumber) {
365
+ debitTotal += view.amount;
384
366
  }
385
- if (transaction.creditAccount === account.accountNumber) {
386
- creditTotal += transaction.amount;
367
+ if (view.creditAccount === account.accountNumber) {
368
+ creditTotal += view.amount;
387
369
  }
388
370
  }
389
371
 
@@ -489,44 +471,10 @@ export class Reports {
489
471
  private async getAccountTransactions(
490
472
  accountNumber: string,
491
473
  params?: IReportParams,
492
- ): Promise<Transaction[]> {
493
- let transactions = await Transaction.getTransactionsByAccount(
494
- accountNumber,
495
- this.skrType,
496
- );
497
-
498
- // Apply date filter
499
- if (params?.dateFrom || params?.dateTo) {
500
- // Normalize dates for inclusive comparison
501
- const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
502
- const dateTo = params.dateTo ? new Date(params.dateTo) : null;
503
-
504
- // Set dateFrom to start of day (00:00:00.000)
505
- if (dateFrom) {
506
- dateFrom.setHours(0, 0, 0, 0);
507
- }
508
-
509
- // Set dateTo to end of day (23:59:59.999) for inclusive comparison
510
- if (dateTo) {
511
- dateTo.setHours(23, 59, 59, 999);
512
- }
513
-
514
- transactions = transactions.filter((transaction) => {
515
- const txDate = transaction.date instanceof Date
516
- ? transaction.date
517
- : new Date(transaction.date);
518
- const txTime = txDate.getTime();
519
-
520
- if (dateFrom && txTime < dateFrom.getTime()) return false;
521
- if (dateTo && txTime > dateTo.getTime()) return false;
522
- return true;
523
- });
524
- }
525
-
526
- // Sort by date
527
- transactions.sort((a, b) => a.date.getTime() - b.date.getTime());
528
-
529
- return transactions;
474
+ ): Promise<ITransactionView[]> {
475
+ // v2: transaction-shaped line views from hashed journal entries,
476
+ // already date-filtered (inclusive day range) and sorted
477
+ return accountLineViews(accountNumber, this.skrType, params);
530
478
  }
531
479
 
532
480
  /**
@@ -744,41 +692,21 @@ export class Reports {
744
692
  * Export to DATEV format
745
693
  */
746
694
  public async exportToDATEV(params?: IReportParams): Promise<string> {
747
- // DATEV format is specific to German accounting software
748
- // This is a simplified implementation
749
- const transactions = await Transaction.getInstances({
750
- skrType: this.skrType,
751
- status: 'posted',
695
+ // EXTF 700 / Formatversion 13 over the v2 (hashed) journal entries
696
+ const dateFrom = params?.dateFrom ?? new Date('1970-01-01T00:00:00.000Z');
697
+ const dateTo = params?.dateTo ?? new Date('2999-12-31T00:00:00.000Z');
698
+ const drafts = await loadDraftsForExport(this.skrType, dateFrom, dateTo);
699
+ const rows = core.journalDraftsToDatevRows(drafts);
700
+ const { csv } = core.writeExtfBuchungsstapel(rows, {
701
+ createdAt: new Date(),
702
+ consultantNumber: 0,
703
+ clientNumber: 0,
704
+ fiscalYearStart: new Date(Date.UTC(dateFrom.getUTCFullYear(), 0, 1)),
705
+ dateFrom,
706
+ dateTo,
707
+ label: 'Buchungsstapel',
708
+ festschreibung: false,
752
709
  });
753
-
754
- const lines: string[] = [];
755
-
756
- // DATEV header
757
- lines.push('EXTF;510;21;"Buchungsstapel";1;;;;;;;;;;;;;;');
758
-
759
- for (const transaction of transactions) {
760
- const date = transaction.date
761
- .toISOString()
762
- .split('T')[0]
763
- .replace(/-/g, '');
764
- const line = [
765
- transaction.amount.toFixed(2).replace('.', ','),
766
- 'S',
767
- 'EUR',
768
- '',
769
- '',
770
- transaction.debitAccount,
771
- transaction.creditAccount,
772
- '',
773
- date,
774
- '',
775
- transaction.description.substring(0, 60),
776
- '',
777
- ].join(';');
778
-
779
- lines.push(line);
780
- }
781
-
782
- return lines.join('\n');
710
+ return csv;
783
711
  }
784
712
  }
@@ -3,6 +3,9 @@ import type { IDatabaseConfig } from './skr.types.js';
3
3
 
4
4
  let dbInstance: plugins.smartdata.SmartdataDb | null = null;
5
5
 
6
+ /** MongoDB ClientSession type, derived from smartdata to avoid a direct mongodb dependency. */
7
+ export type TDbSession = ReturnType<plugins.smartdata.SmartdataDb['startSession']>;
8
+
6
9
  export const getDb = async (
7
10
  config?: IDatabaseConfig,
8
11
  ): Promise<plugins.smartdata.SmartdataDb> => {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Typed errors for the v2 posting path so callers can react precisely.
3
+ */
4
+
5
+ export class ImmutableEntryError extends Error {
6
+ constructor(entryId: string) {
7
+ super(
8
+ `Journal entry ${entryId} is festgeschrieben (hashed) and immutable — corrections only via Storno entries`,
9
+ );
10
+ this.name = 'ImmutableEntryError';
11
+ }
12
+ }
13
+
14
+ export class PeriodLockedError extends Error {
15
+ constructor(skrType: string, period: string) {
16
+ super(`Period ${period} (${skrType}) is festgeschrieben — no further postings allowed`);
17
+ this.name = 'PeriodLockedError';
18
+ }
19
+ }
20
+
21
+ export class SequenceConflictError extends Error {
22
+ constructor(skrType: string, fiscalYear: number) {
23
+ super(
24
+ `Hash-chain head moved concurrently for ${skrType}/${fiscalYear} — transaction aborted`,
25
+ );
26
+ this.name = 'SequenceConflictError';
27
+ }
28
+ }
29
+
30
+ export class NotImplementedError extends Error {
31
+ constructor(feature: string) {
32
+ super(feature);
33
+ this.name = 'NotImplementedError';
34
+ }
35
+ }
36
+
37
+ export class TransactionsUnsupportedError extends Error {
38
+ constructor(detail: string) {
39
+ super(
40
+ `MongoDB deployment does not support transactions (replica set required) — filing-grade posting has no non-transactional fallback. ${detail}`,
41
+ );
42
+ this.name = 'TransactionsUnsupportedError';
43
+ }
44
+ }
package/ts/skr.export.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import * as path from 'path';
3
+ import { createHash } from 'crypto';
3
4
  import type { IAccountData, ITransactionData, IJournalEntry, TSKRType } from './skr.types.js';
4
5
 
5
6
  export interface IExportOptions {
@@ -370,12 +371,8 @@ COMPLIANCE
370
371
  Buffer.from(hash, 'hex')
371
372
  );
372
373
 
373
- // Create a sync hash function wrapper for MerkleTree
374
- const hashFn = (data: Buffer) => {
375
- // Convert async to sync by using crypto directly
376
- const crypto = require('crypto');
377
- return crypto.createHash('sha256').update(data).digest();
378
- };
374
+ // Sync SHA-256 for MerkleTree (ESM import require() is unavailable here)
375
+ const hashFn = (data: Buffer) => createHash('sha256').update(data).digest();
379
376
 
380
377
  const tree = new plugins.MerkleTree(leaves, hashFn, {
381
378
  sortPairs: true