@fin.cx/skr 1.3.1 → 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 (90) 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.booking.js +14 -28
  42. package/dist_ts/skr.migrate.d.ts +20 -0
  43. package/dist_ts/skr.migrate.js +223 -0
  44. package/dist_ts/skr.period.d.ts +47 -0
  45. package/dist_ts/skr.period.js +95 -0
  46. package/dist_ts/skr.posting.d.ts +74 -0
  47. package/dist_ts/skr.posting.js +209 -0
  48. package/dist_ts/skr.security.d.ts +6 -9
  49. package/dist_ts/skr.security.js +16 -89
  50. package/dist_ts/skr.sequence.d.ts +32 -0
  51. package/dist_ts/skr.sequence.js +51 -0
  52. package/dist_ts/skr.txview.d.ts +46 -0
  53. package/dist_ts/skr.txview.js +235 -0
  54. package/dist_ts/skr.types.d.ts +35 -1
  55. package/dist_ts/skr.verify.d.ts +13 -0
  56. package/dist_ts/skr.verify.js +84 -0
  57. package/package.json +5 -4
  58. package/readme.hints.md +37 -0
  59. package/readme.md +69 -1
  60. package/readme.plan.md +180 -243
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/core/core.accountpolicy.ts +154 -0
  63. package/ts/core/core.bukeys.ts +146 -0
  64. package/ts/core/core.datev.captions.ts +133 -0
  65. package/ts/core/core.datev.ts +174 -0
  66. package/ts/core/core.hash.ts +76 -0
  67. package/ts/core/core.plugins.ts +9 -0
  68. package/ts/core/core.recipes.ts +659 -0
  69. package/ts/core/core.taxscenario.ts +156 -0
  70. package/ts/core/core.types.ts +74 -0
  71. package/ts/core/core.validate.ts +119 -0
  72. package/ts/core/index.ts +15 -0
  73. package/ts/index.ts +31 -0
  74. package/ts/plugins.ts +4 -0
  75. package/ts/skr.api.ts +432 -46
  76. package/ts/skr.balances.ts +127 -0
  77. package/ts/skr.classes.journalentry.ts +54 -5
  78. package/ts/skr.classes.reports.ts +54 -126
  79. package/ts/skr.database.ts +3 -0
  80. package/ts/skr.errors.ts +44 -0
  81. package/ts/skr.export.ts +3 -6
  82. package/ts/skr.invoice.booking.ts +22 -34
  83. package/ts/skr.migrate.ts +268 -0
  84. package/ts/skr.period.ts +139 -0
  85. package/ts/skr.posting.ts +273 -0
  86. package/ts/skr.security.ts +18 -93
  87. package/ts/skr.sequence.ts +89 -0
  88. package/ts/skr.txview.ts +268 -0
  89. package/ts/skr.types.ts +37 -1
  90. package/ts/skr.verify.ts +99 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Bridges between the legacy v1 shapes (float euros, numeric posting keys,
3
+ * debit/credit account pairs) and the v2 ledger (cents, string BU keys,
4
+ * per-line Gegenkonto):
5
+ *
6
+ * - input converters: ITransactionData / IJournalEntry -> core.IJournalDraft
7
+ * - output projections: v2 JournalEntry (lines) -> transaction-shaped views
8
+ *
9
+ * The Transaction collection is FROZEN in v2 — these projections are what
10
+ * keeps transaction-style reads working.
11
+ */
12
+ import * as plugins from './plugins.js';
13
+ import * as core from './core/index.js';
14
+ import { getDbSync } from './skr.database.js';
15
+ import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
16
+ const { centsFromFloat } = plugins.calculation;
17
+ /** v1 numeric posting keys that map 1:1 onto the v2 string BU keys */
18
+ const NUMERIC_KEY_MAP = {
19
+ 1: '1',
20
+ 2: '2',
21
+ 3: '3',
22
+ 8: '8',
23
+ 9: '9',
24
+ 18: '18',
25
+ 19: '19',
26
+ 40: '40',
27
+ 91: '91',
28
+ 94: '94',
29
+ };
30
+ export function mapNumericPostingKey(postingKey) {
31
+ if (postingKey === undefined || postingKey === null)
32
+ return '';
33
+ return NUMERIC_KEY_MAP[postingKey] ?? '';
34
+ }
35
+ /**
36
+ * v1 postTransaction input -> two-line v2 draft. The amount is NOT split into
37
+ * net/VAT (v1 semantics: vatAmount was informational) so ledger figures stay
38
+ * identical across the migration.
39
+ */
40
+ export function transactionDataToDraft(data) {
41
+ const amountCents = centsFromFloat(data.amount);
42
+ const vatCents = data.vatAmount ? centsFromFloat(data.vatAmount) : 0;
43
+ const lines = [
44
+ {
45
+ accountNumber: data.debitAccount,
46
+ counterAccount: data.creditAccount,
47
+ side: 'debit',
48
+ amountCents,
49
+ buKey: '',
50
+ buchungstext: data.description,
51
+ belegfeld1: data.reference,
52
+ costCenter1: data.costCenter,
53
+ },
54
+ {
55
+ accountNumber: data.creditAccount,
56
+ counterAccount: data.debitAccount,
57
+ side: 'credit',
58
+ amountCents,
59
+ buKey: '',
60
+ buchungstext: data.description,
61
+ belegfeld1: data.reference,
62
+ costCenter1: data.costCenter,
63
+ },
64
+ ];
65
+ lines[0].vatAmountCents = vatCents;
66
+ return {
67
+ date: data.date,
68
+ description: data.description,
69
+ reference: data.reference,
70
+ skrType: data.skrType,
71
+ currency: 'EUR',
72
+ lines,
73
+ };
74
+ }
75
+ /**
76
+ * v1 postJournalEntry input -> v2 draft. Gegenkonto derivation: when one side
77
+ * of the entry has exactly one line, it is the counter account for every line
78
+ * of the other side (the dominant v1 usage); otherwise the first line of the
79
+ * opposite side is used (documented heuristic — v1 stored no Gegenkonto).
80
+ */
81
+ export function journalInputToDraft(entry) {
82
+ const debitLines = entry.lines.filter((line) => (line.debit ?? 0) > 0);
83
+ const creditLines = entry.lines.filter((line) => (line.credit ?? 0) > 0);
84
+ const counterFor = (side) => {
85
+ const opposite = side === 'debit' ? creditLines : debitLines;
86
+ return opposite[0]?.accountNumber ?? '';
87
+ };
88
+ const lines = entry.lines.map((line) => {
89
+ const isDebit = (line.debit ?? 0) > 0;
90
+ return {
91
+ accountNumber: line.accountNumber,
92
+ counterAccount: counterFor(isDebit ? 'debit' : 'credit'),
93
+ side: isDebit ? 'debit' : 'credit',
94
+ amountCents: centsFromFloat(isDebit ? line.debit : line.credit),
95
+ buKey: mapNumericPostingKey(line.postingKey),
96
+ buchungstext: line.description ?? entry.description,
97
+ costCenter1: line.costCenter,
98
+ };
99
+ });
100
+ return {
101
+ date: entry.date,
102
+ description: entry.description,
103
+ reference: entry.reference,
104
+ skrType: entry.skrType,
105
+ currency: 'EUR',
106
+ lines,
107
+ };
108
+ }
109
+ /**
110
+ * Project a two-line v2 entry back into the classic transaction shape.
111
+ * Returns null for entries with more than two lines (journal-only view).
112
+ */
113
+ export function entryToTransactionView(entry) {
114
+ const debitLine = entry.lines.find((line) => line.side === 'debit');
115
+ const creditLine = entry.lines.find((line) => line.side === 'credit');
116
+ if (!debitLine || !creditLine || entry.lines.length !== 2)
117
+ return null;
118
+ return {
119
+ id: entry.id,
120
+ transactionNumber: entry.journalNumber ?? entry.id,
121
+ date: entry.date,
122
+ debitAccount: debitLine.accountNumber,
123
+ creditAccount: creditLine.accountNumber,
124
+ amount: (debitLine.amountCents ?? 0) / 100,
125
+ vatAmount: (debitLine.vatAmountCents ?? 0) / 100,
126
+ description: entry.description,
127
+ reference: entry.reference ?? '',
128
+ skrType: entry.skrType,
129
+ status: entry.status,
130
+ period: entry.period ?? '',
131
+ fiscalYear: entry.fiscalYear ?? entry.date.getUTCFullYear(),
132
+ costCenter: debitLine.costCenter1 ?? creditLine.costCenter1,
133
+ reversalOf: entry.reversalOf,
134
+ };
135
+ }
136
+ function dayRange(params) {
137
+ const from = params?.dateFrom ? new Date(params.dateFrom) : null;
138
+ const to = params?.dateTo ? new Date(params.dateTo) : null;
139
+ if (from)
140
+ from.setHours(0, 0, 0, 0);
141
+ if (to)
142
+ to.setHours(23, 59, 59, 999);
143
+ return { from, to };
144
+ }
145
+ /**
146
+ * Load v2 entries of a date range as core drafts (for DATEV export).
147
+ */
148
+ export async function loadDraftsForExport(skrType, dateFrom, dateTo) {
149
+ const docs = await getDbSync()
150
+ .mongoDb.collection(JOURNAL_ENTRY_COLLECTION)
151
+ .find({
152
+ skrType,
153
+ schemaVersion: 2,
154
+ status: { $in: ['posted', 'finalized'] },
155
+ date: { $gte: dateFrom, $lte: dateTo },
156
+ })
157
+ .sort({ sequenceNumber: 1 })
158
+ .toArray();
159
+ return docs.map((doc) => {
160
+ const entry = doc;
161
+ return {
162
+ date: entry.date,
163
+ description: entry.description,
164
+ reference: entry.reference,
165
+ skrType: entry.skrType,
166
+ currency: 'EUR',
167
+ lines: entry.lines.map((line) => ({
168
+ accountNumber: line.accountNumber,
169
+ counterAccount: line.counterAccount ?? '',
170
+ side: (line.side ?? 'debit'),
171
+ amountCents: line.amountCents ?? 0,
172
+ buKey: (line.buKey ?? ''),
173
+ buchungstext: line.buchungstext ?? entry.description,
174
+ belegfeld1: line.belegfeld1,
175
+ costCenter1: line.costCenter1,
176
+ docLink: line.docLink,
177
+ })),
178
+ };
179
+ });
180
+ }
181
+ /**
182
+ * Per-account activity as transaction-shaped line views from v2 entries —
183
+ * the data source for reports (trial balance, general ledger, cash flow).
184
+ * Each view carries the line's own side and its Gegenkonto.
185
+ */
186
+ export async function accountLineViews(accountNumber, skrType, params) {
187
+ const { from, to } = dayRange(params);
188
+ const match = {
189
+ skrType,
190
+ schemaVersion: 2,
191
+ status: { $in: ['posted', 'finalized'] },
192
+ 'lines.accountNumber': accountNumber,
193
+ };
194
+ if (from || to) {
195
+ const range = {};
196
+ if (from)
197
+ range.$gte = from;
198
+ if (to)
199
+ range.$lte = to;
200
+ match.date = range;
201
+ }
202
+ const docs = await getDbSync()
203
+ .mongoDb.collection(JOURNAL_ENTRY_COLLECTION)
204
+ .find(match)
205
+ .sort({ date: 1, sequenceNumber: 1 })
206
+ .toArray();
207
+ const views = [];
208
+ for (const doc of docs) {
209
+ const entry = doc;
210
+ for (const line of entry.lines) {
211
+ if (line.accountNumber !== accountNumber)
212
+ continue;
213
+ const isDebit = line.side === 'debit';
214
+ views.push({
215
+ id: entry.id,
216
+ transactionNumber: entry.journalNumber ?? entry.id,
217
+ date: entry.date,
218
+ debitAccount: isDebit ? accountNumber : line.counterAccount ?? '',
219
+ creditAccount: isDebit ? line.counterAccount ?? '' : accountNumber,
220
+ amount: (line.amountCents ?? 0) / 100,
221
+ vatAmount: (line.vatAmountCents ?? 0) / 100,
222
+ description: line.buchungstext ?? entry.description,
223
+ reference: entry.reference ?? '',
224
+ skrType: entry.skrType,
225
+ status: entry.status,
226
+ period: entry.period ?? '',
227
+ fiscalYear: entry.fiscalYear ?? entry.date.getUTCFullYear(),
228
+ costCenter: line.costCenter1,
229
+ reversalOf: entry.reversalOf,
230
+ });
231
+ }
232
+ }
233
+ return views;
234
+ }
235
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLnR4dmlldy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3Nrci50eHZpZXcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7R0FVRztBQUNILE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0saUJBQWlCLENBQUM7QUFDeEMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzlDLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBVTNELE1BQU0sRUFBRSxjQUFjLEVBQUUsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDO0FBRS9DLHNFQUFzRTtBQUN0RSxNQUFNLGVBQWUsR0FBZ0M7SUFDbkQsQ0FBQyxFQUFFLEdBQUc7SUFDTixDQUFDLEVBQUUsR0FBRztJQUNOLENBQUMsRUFBRSxHQUFHO0lBQ04sQ0FBQyxFQUFFLEdBQUc7SUFDTixDQUFDLEVBQUUsR0FBRztJQUNOLEVBQUUsRUFBRSxJQUFJO0lBQ1IsRUFBRSxFQUFFLElBQUk7SUFDUixFQUFFLEVBQUUsSUFBSTtJQUNSLEVBQUUsRUFBRSxJQUFJO0lBQ1IsRUFBRSxFQUFFLElBQUk7Q0FDVCxDQUFDO0FBRUYsTUFBTSxVQUFVLG9CQUFvQixDQUFDLFVBQW1CO0lBQ3RELElBQUksVUFBVSxLQUFLLFNBQVMsSUFBSSxVQUFVLEtBQUssSUFBSTtRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQy9ELE9BQU8sZUFBZSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUMzQyxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxJQUFzQjtJQUMzRCxNQUFNLFdBQVcsR0FBRyxjQUFjLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ2hELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLGNBQWMs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
@@ -53,7 +53,41 @@ export interface IJournalEntryLine {
53
53
  credit?: number;
54
54
  description?: string;
55
55
  costCenter?: string;
56
- postingKey: TPostingKey;
56
+ /** v1 numeric DATEV posting key; optional since schemaVersion 2 (see buKey) */
57
+ postingKey?: TPostingKey;
58
+ side?: 'debit' | 'credit';
59
+ amountCents?: number;
60
+ counterAccount?: string;
61
+ /** string-typed DATEV BU-Schlüssel ('' = none) */
62
+ buKey?: string;
63
+ buchungstext?: string;
64
+ belegfeld1?: string;
65
+ costCenter1?: string;
66
+ docLink?: string;
67
+ /** informational VAT share carried on the line (v1 compatibility) */
68
+ vatAmountCents?: number;
69
+ }
70
+ /**
71
+ * Transaction-shaped projection of a v2 journal entry (or one of its lines).
72
+ * The Transaction collection is frozen in v2 — reads go through these views.
73
+ */
74
+ export interface ITransactionView {
75
+ id: string;
76
+ transactionNumber: string;
77
+ date: Date;
78
+ debitAccount: string;
79
+ creditAccount: string;
80
+ amount: number;
81
+ vatAmount: number;
82
+ description: string;
83
+ reference: string;
84
+ skrType: TSKRType;
85
+ status: string;
86
+ period: string;
87
+ fiscalYear: number;
88
+ costCenter?: string;
89
+ /** id of the entry this one reverses (Storno linkage) */
90
+ reversalOf?: string;
57
91
  }
58
92
  export interface ITrialBalanceEntry {
59
93
  accountNumber: string;
@@ -0,0 +1,13 @@
1
+ import type { TSKRType } from './skr.types.js';
2
+ export interface IChainVerificationResult {
3
+ valid: boolean;
4
+ entriesChecked: number;
5
+ issues: string[];
6
+ headHash: string | null;
7
+ }
8
+ /**
9
+ * Walk sequence 1..N for (skrType, fiscalYear): check gaplessness, recompute
10
+ * every entry hash from the persisted payload, verify the chain links, and
11
+ * compare the final hash against the stored chain head.
12
+ */
13
+ export declare function verifyHashChain(skrType: TSKRType, fiscalYear: number): Promise<IChainVerificationResult>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Cryptographic audit-trail verification. Prevention of tampering is
3
+ * best-effort (doc-level guards); detection is this module: recompute the
4
+ * whole hash chain from the persisted entries and compare against the stored
5
+ * chain head.
6
+ */
7
+ import * as core from './core/index.js';
8
+ import { getDbSync } from './skr.database.js';
9
+ import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
10
+ import { getSequenceState } from './skr.sequence.js';
11
+ import { buildHashPayload } from './skr.posting.js';
12
+ /**
13
+ * Walk sequence 1..N for (skrType, fiscalYear): check gaplessness, recompute
14
+ * every entry hash from the persisted payload, verify the chain links, and
15
+ * compare the final hash against the stored chain head.
16
+ */
17
+ export async function verifyHashChain(skrType, fiscalYear) {
18
+ const issues = [];
19
+ const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
20
+ const docs = await collection
21
+ .find({ skrType, fiscalYear, schemaVersion: 2 })
22
+ .sort({ sequenceNumber: 1 })
23
+ .toArray();
24
+ const state = await getSequenceState(skrType, fiscalYear);
25
+ if (docs.length === 0) {
26
+ return {
27
+ valid: !state || state.value === 0,
28
+ entriesChecked: 0,
29
+ issues: state && state.value !== 0 ? [`sequence counter is ${state.value} but no entries exist`] : [],
30
+ headHash: state?.headHash ?? null,
31
+ };
32
+ }
33
+ // gaplessness
34
+ docs.forEach((doc, index) => {
35
+ const expected = index + 1;
36
+ if (doc.sequenceNumber !== expected) {
37
+ issues.push(`sequence gap: expected ${expected}, found ${doc.sequenceNumber}`);
38
+ }
39
+ });
40
+ // chain
41
+ const genesis = core.genesisHash(skrType, fiscalYear);
42
+ const chain = docs.map((doc) => ({
43
+ sequenceNumber: doc.sequenceNumber,
44
+ prevHash: doc.prevHash,
45
+ entryHash: doc.entryHash,
46
+ payload: buildHashPayload({
47
+ sequenceNumber: doc.sequenceNumber,
48
+ skrType: doc.skrType,
49
+ fiscalYear: doc.fiscalYear,
50
+ date: doc.date,
51
+ postedAt: doc.postedAt,
52
+ description: doc.description,
53
+ reference: doc.reference,
54
+ taxScenario: doc.taxScenario,
55
+ currency: doc.currency,
56
+ reversalOf: doc.reversalOf,
57
+ lines: (doc.lines ?? []),
58
+ }),
59
+ }));
60
+ const failure = core.verifyChain(chain, genesis);
61
+ if (failure) {
62
+ issues.push(failure.reason);
63
+ }
64
+ // head comparison
65
+ const lastHash = chain[chain.length - 1].entryHash;
66
+ if (!state) {
67
+ issues.push('no sequence state document found');
68
+ }
69
+ else {
70
+ if (state.headHash !== lastHash) {
71
+ issues.push(`stored chain head ${state.headHash} !== last entry hash ${lastHash}`);
72
+ }
73
+ if (state.value !== docs.length) {
74
+ issues.push(`sequence counter ${state.value} !== entry count ${docs.length}`);
75
+ }
76
+ }
77
+ return {
78
+ valid: issues.length === 0,
79
+ entriesChecked: docs.length,
80
+ issues,
81
+ headHash: state?.headHash ?? null,
82
+ };
83
+ }
84
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLnZlcmlmeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3Nrci52ZXJpZnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7O0dBS0c7QUFDSCxPQUFPLEtBQUssSUFBSSxNQUFNLGlCQUFpQixDQUFDO0FBQ3hDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUMzRCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQVVwRDs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxlQUFlLENBQ25DLE9BQWlCLEVBQ2pCLFVBQWtCO0lBRWxCLE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixNQUFNLFVBQVUsR0FBRyxTQUFTLEVBQUUsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLHdCQUF3QixDQUFDLENBQUM7SUFDNUUsTUFBTSxJQUFJLEdBQUcsTUFBTSxVQUFVO1NBQzFCLElBQUksQ0FBQyxFQUFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsYUFBYSxFQUFFLENBQUMsRUFBRSxDQUFDO1NBQy9DLElBQUksQ0FBQyxFQUFFLGNBQWMsRUFBRSxDQUFDLEVBQUUsQ0FBQztTQUMzQixPQUFPLEVBQUUsQ0FBQztJQUViLE1BQU0sS0FBSyxHQUFHLE1BQU0sZ0JBQWdCLENBQUMsT0FBTyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQzFELElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN0QixPQUFPO1lBQ0wsS0FBSyxFQUFFLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxLQUFLLEtBQUssQ0FBQztZQUNsQyxjQUFjLEVBQUUsQ0FBQztZQUNqQixNQUFNLEVBQUUsS0FBSyxJQUFJLEtBQUss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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/skr",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
5
5
  "main": "dist_ts/index.js",
6
6
  "typings": "dist_ts/index.d.ts",
@@ -20,8 +20,9 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "@e-invoice-eu/core": "^3.1.1",
23
+ "@fin.cx/calculation": "^1.1.1",
23
24
  "@fin.cx/einvoice": "6.0.0",
24
- "@push.rocks/smartdata": "^7.1.7",
25
+ "@push.rocks/smartdata": "^7.2.1",
25
26
  "@push.rocks/smartfile": "^13.1.3",
26
27
  "@push.rocks/smartfs": "^1.5.1",
27
28
  "@push.rocks/smarthash": "^3.2.7",
@@ -35,10 +36,10 @@
35
36
  },
36
37
  "devDependencies": {
37
38
  "@git.zone/tsbuild": "^4.4.2",
38
- "@git.zone/tsrun": "^2.0.4",
39
+ "@git.zone/tsrun": "^2.0.5",
39
40
  "@git.zone/tstest": "^3.6.6",
40
41
  "@push.rocks/qenv": "^6.1.4",
41
- "@types/node": "^25.9.1",
42
+ "@types/node": "^26.1.1",
42
43
  "@types/node-forge": "^1.3.14"
43
44
  },
44
45
  "repository": {
package/readme.hints.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Project Readme Hints
2
2
 
3
+ ## Dev Environment (2026-07-09)
4
+
5
+ - Local services are managed by `gitzone services` (@git.zone/cli >= 2.23.0); the old
6
+ `services.sh` was removed. `gitzone services start` creates MongoDB as a **single-node
7
+ replica set** (rs0, keyfile auth, mongod on the mapped port inside the container) —
8
+ required because skr v2 posting uses MongoDB transactions. Verify with
9
+ `.nogit/debug/check-replicaset.ts` (tsx).
10
+ - Service selection lives in `.smartconfig.json` under `@git.zone/cli.services`;
11
+ runtime config in `.nogit/env.json`. Old standalone data was backed up to
12
+ `.nogit/mongodata.pre-replset.bak` (disposable test data).
13
+ - Program plan for the accounting roadmap ("no external tax accountant") is in
14
+ `readme.plan.md`; M0 = skr v2 core hardening. @fin.cx/calculation ships the cents
15
+ module (TCents) that skr v2 money handling builds on.
16
+
17
+ ## M0 Phase 2 — stateless core (2026-07-09)
18
+
19
+ - `ts/core/` is the pure booking core (exported as `core` from the package index):
20
+ types, BU keys, tax-scenario cascade, account policies, recipes, validation,
21
+ DATEV EXTF writer, hash chain. Hard rule: core imports only @fin.cx/calculation
22
+ (+ smarthash for SHA-256) — no smartdata/fs/network.
23
+ - **BU-key corrections vs v1/finance.plus** (cross-checked 2026-07-09): 18/19 =
24
+ steuerpflichtiger innergemeinschaftlicher Erwerb 7%/19% (§1a UStG); 91/94 =
25
+ Leistungsempfänger schuldet die Steuer 7%/19% (§13b UStG); 40 = Aufhebung der
26
+ Automatik. v1's `skr.postingkeys.ts` (numeric keys with partly different meanings)
27
+ is superseded by `ts/core/core.bukeys.ts`. Official DATEV table (Hilfe-Center doc
28
+ 0904313) is portal-gated — final verification = manual DATEV import per RC.
29
+ - **DATEV EXTF v13 facts** (verified against a DATEV-importable reference file):
30
+ 125 data columns, 31 header fields, field 114 = Festschreibung, decimal-comma
31
+ amounts, Belegdatum TTMM, text fields quoted, CRLF; file encoding must be CP1252
32
+ (conversion happens at the IO layer, core emits a JS string).
33
+ `ts/core/core.datev.captions.ts` is generated from that reference — do not edit.
34
+ - Automatik convention in recipes: a line on an Automatikkonto carries key '' when
35
+ the entry has no explicit VAT lines, and key '40' when explicit VAT lines exist
36
+ (automatism must be cancelled so DATEV import doesn't double the tax).
37
+ - PROVISIONAL policy accounts (verify in M-A): reduced-rate Skonto accounts
38
+ (3731/8731, 5731/4731) and rounding-diff accounts (2700/2300, 4830/6300).
39
+
3
40
  ## Current Status (2025-10-27)
4
41
 
5
42
  ### Test Results
package/readme.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  It is built for developers who need a programmable accounting core instead of a pile of CSV glue code: initialize a chart of accounts, post validated transactions and journal entries, generate reports, and archive year-end data in a structured export format.
6
6
 
7
+ **Version 2** makes the ledger filing-grade:
8
+
9
+ - Exact integer-cent amounts internally (no float drift; balance checks are `===`)
10
+ - Atomic posting via MongoDB transactions (replica set required)
11
+ - GoBD audit trail: gapless sequence numbers, SHA-256 hash chain, immutable posted entries, Storno-only corrections
12
+ - Festschreibung: period locking with cryptographic chain verification
13
+ - A pure, stateless booking core (`core` export): BU keys, tax-scenario detection, booking recipes, validation — no database needed
14
+ - DATEV Buchungsstapel export in the real EXTF 700 / Formatversion 13 layout (125 columns)
15
+
16
+ ### Breaking changes in v2
17
+
18
+ - MongoDB must run as a **replica set** (single-node is fine); `initialize()` fails hard without transaction support. `gitzone services start` creates a suitable MongoDB automatically.
19
+ - `postTransaction()` / `reverseTransaction()` return transaction-shaped views (`ITransactionView`), not `Transaction` documents; the Transaction collection is frozen as a legacy read model.
20
+ - Posted entries are immutable: reversing never mutates the original (`status` stays `posted`; linkage via `reversalOf`).
21
+ - `exportToDATEV()` now emits the EXTF 700 format; persist the string as **CP1252**.
22
+ - `IJournalEntryLine.postingKey` is optional; the v2 line fields (`side`, `amountCents`, `counterAccount`, string `buKey`) are authoritative.
23
+ - Existing v1 data: run `api.migrateToV2()` once (idempotent; keeps all legacy fields; returns a report to archive).
24
+
7
25
  ## Issue Reporting and Security
8
26
 
9
27
  For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
@@ -29,7 +47,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
29
47
  ## Requirements
30
48
 
31
49
  - Node.js 20+ with ESM support
32
- - A reachable MongoDB instance
50
+ - A reachable MongoDB instance running as a **replica set** (transactions are
51
+ mandatory for atomic posting; a single-node replica set is sufficient —
52
+ `gitzone services start` provides one locally)
33
53
  - `pnpm`
34
54
 
35
55
  The test setup reads MongoDB connection details from `.nogit/` via `@push.rocks/qenv`, but the runtime API only needs a `mongoDbUrl` and an optional `dbName`.
@@ -198,6 +218,54 @@ const imported = await api.importAccountsFromCSV(csvContent);
198
218
  const accountCsv = await api.exportAccountsToCSV();
199
219
  ```
200
220
 
221
+ ## v2 Ledger: Recipes, Festschreibung, Audit Chain
222
+
223
+ The stateless core builds validated drafts; the poster writes them atomically:
224
+
225
+ ```ts
226
+ import { SkrApi, core } from '@fin.cx/skr';
227
+
228
+ const api = new SkrApi({ mongoDbUrl, dbName });
229
+ await api.initialize('SKR03');
230
+ const policy = api.getDefaultPolicy();
231
+
232
+ // booking recipes are pure functions — usable without any database
233
+ const draft = core.bookVendorInvoice(
234
+ {
235
+ date: new Date(),
236
+ vendorAccount: '70001',
237
+ grossCents: 11900,
238
+ scenario: 'domestic_standard',
239
+ invoiceNumber: 'RE-1001',
240
+ description: 'Bürobedarf',
241
+ },
242
+ policy,
243
+ );
244
+
245
+ const entry = await api.postDraft(draft); // gapless sequence + hash chain
246
+ const storno = await api.postStorno(entry); // corrections never mutate
247
+
248
+ await api.festschreibePeriod('2026-03'); // GoBD period lock
249
+ const audit = await api.verifyAuditChain(2026); // recompute the whole chain
250
+ const balance = await api.getAccountBalanceCents('1200'); // exact cents
251
+
252
+ const { csv, filename } = await api.exportDatevBuchungsstapel({
253
+ dateFrom: new Date('2026-03-01'),
254
+ dateTo: new Date('2026-03-31'),
255
+ consultantNumber: 1001,
256
+ clientNumber: 456,
257
+ fiscalYearStart: new Date('2026-01-01'),
258
+ festschreibung: true,
259
+ });
260
+ // persist csv as CP1252 — DATEV expects ANSI encoding
261
+ ```
262
+
263
+ Also in `core`: `bookCustomerInvoice` (incl. intra-EU supply, export, §13b),
264
+ `bookBankPayment` (Skonto with VAT correction, tolerance write-offs),
265
+ `bookInternalTransfer` (Geldtransit), `bookToSuspense`/`resolveSuspense`,
266
+ `fxDifferenceLines`, `detectTaxScenario` (confidence-scored cascade), the
267
+ BU-key table (`BU_KEY_RULES`), and `validateJournalDraft`.
268
+
201
269
  ## Reports And Exports
202
270
 
203
271
  Available reporting methods on `SkrApi`: