@superheld/summae-core 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,4328 @@
1
+ import Big from 'big.js';
2
+ import { randomBytes, createHash } from 'crypto';
3
+
4
+ // src/shared/errors.ts
5
+ var InvalidValue = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "InvalidValue";
9
+ }
10
+ };
11
+ var CurrencyMismatch = class extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "CurrencyMismatch";
15
+ }
16
+ };
17
+
18
+ // src/shared/currency.ts
19
+ var SCALES = {
20
+ JPY: 0,
21
+ KRW: 0,
22
+ BHD: 3,
23
+ KWD: 3,
24
+ TND: 3
25
+ };
26
+ var Currency = class _Currency {
27
+ constructor(code, scale) {
28
+ this.code = code;
29
+ this.scale = scale;
30
+ }
31
+ code;
32
+ scale;
33
+ static of(code) {
34
+ if (!/^[A-Z]{3}$/.test(code)) {
35
+ throw new InvalidValue(`Ung\xFCltiger ISO-4217-Code: "${code}"`);
36
+ }
37
+ return new _Currency(code, SCALES[code] ?? 2);
38
+ }
39
+ equals(other) {
40
+ return this.code === other.code;
41
+ }
42
+ toJSON() {
43
+ return this.code;
44
+ }
45
+ toString() {
46
+ return this.code;
47
+ }
48
+ };
49
+ var HALF_UP = Big.roundHalfUp;
50
+ function decimalPlaces(value) {
51
+ return Math.max(0, value.c.length - value.e - 1);
52
+ }
53
+ var Money = class _Money {
54
+ constructor(amount, currency) {
55
+ this.amount = amount;
56
+ this.currency = currency;
57
+ }
58
+ amount;
59
+ currency;
60
+ static currencyOf(currency) {
61
+ return currency instanceof Currency ? currency : Currency.of(currency);
62
+ }
63
+ /**
64
+ * Exakter Betrag auf Währungsskala. Mehr Nachkommastellen als die Währung
65
+ * erlaubt sind ein Fehler — hier wird nie still gerundet.
66
+ */
67
+ static of(amount, currency) {
68
+ const cur = _Money.currencyOf(currency);
69
+ let value;
70
+ try {
71
+ value = new Big(amount);
72
+ } catch {
73
+ throw new InvalidValue(`Ung\xFCltiger Betrag "${amount}" f\xFCr W\xE4hrung ${cur.code} (Skala ${cur.scale})`);
74
+ }
75
+ const scaled = value.round(cur.scale, HALF_UP);
76
+ if (!scaled.eq(value)) {
77
+ throw new InvalidValue(`Ung\xFCltiger Betrag "${amount}" f\xFCr W\xE4hrung ${cur.code} (Skala ${cur.scale})`);
78
+ }
79
+ return new _Money(scaled, cur);
80
+ }
81
+ /**
82
+ * Ergebnis einer Rechnung auf Währungsskala bringen: half-up
83
+ * (2.225 → 2.23, -2.345 → -2.35). Einziger Weg, auf dem Money rundet.
84
+ */
85
+ static fromCalculation(value, currency) {
86
+ const cur = _Money.currencyOf(currency);
87
+ let big;
88
+ try {
89
+ big = value instanceof Big ? value : new Big(value);
90
+ } catch {
91
+ throw new InvalidValue(`Ung\xFCltiger Rechenwert f\xFCr W\xE4hrung ${cur.code}`);
92
+ }
93
+ return new _Money(big.round(cur.scale, HALF_UP), cur);
94
+ }
95
+ static zero(currency) {
96
+ return new _Money(new Big(0), _Money.currencyOf(currency));
97
+ }
98
+ add(other) {
99
+ this.assertSameCurrency(other);
100
+ return new _Money(this.amount.plus(other.amount), this.currency);
101
+ }
102
+ subtract(other) {
103
+ this.assertSameCurrency(other);
104
+ return new _Money(this.amount.minus(other.amount), this.currency);
105
+ }
106
+ negate() {
107
+ return new _Money(this.amount.times(-1), this.currency);
108
+ }
109
+ abs() {
110
+ return new _Money(this.amount.abs(), this.currency);
111
+ }
112
+ /** -1, 0 oder 1 */
113
+ compareTo(other) {
114
+ this.assertSameCurrency(other);
115
+ return this.amount.cmp(other.amount);
116
+ }
117
+ equals(other) {
118
+ return this.currency.equals(other.currency) && this.amount.eq(other.amount);
119
+ }
120
+ isZero() {
121
+ return this.amount.eq(0);
122
+ }
123
+ isPositive() {
124
+ return this.amount.gt(0);
125
+ }
126
+ isNegative() {
127
+ return this.amount.lt(0);
128
+ }
129
+ /**
130
+ * Verteilt den Betrag nach Gewichten (determinismus.md §2): Largest-Remainder,
131
+ * Gleichstand → erster Teil. Σ Teile = Betrag, immer.
132
+ *
133
+ * Gewichte: nicht-negative Dezimalwerte (Zahl oder String), Summe > 0.
134
+ * Negative Beträge werden als negiertes Spiegelbild verteilt.
135
+ */
136
+ allocate(...weights) {
137
+ if (weights.length === 0) {
138
+ throw new InvalidValue("allocate braucht mindestens ein Gewicht");
139
+ }
140
+ if (this.isNegative()) {
141
+ return this.negate().allocate(...weights).map((part) => part.negate());
142
+ }
143
+ const scale = this.currency.scale;
144
+ const integerWeights = _Money.normalizeWeights(weights);
145
+ let weightSum = 0n;
146
+ for (const weight of integerWeights) {
147
+ weightSum += weight;
148
+ }
149
+ if (weightSum === 0n) {
150
+ throw new InvalidValue("Gewichtssumme muss > 0 sein");
151
+ }
152
+ const factor = new Big(10).pow(scale);
153
+ const totalMinor = BigInt(this.amount.times(factor).toFixed(0));
154
+ const parts = integerWeights.map((weight) => {
155
+ const product = totalMinor * weight;
156
+ return { base: product / weightSum, remainder: product % weightSum };
157
+ });
158
+ let assigned = 0n;
159
+ for (const part of parts) {
160
+ assigned += part.base;
161
+ }
162
+ const leftover = Number(totalMinor - assigned);
163
+ const order = parts.map((part, index) => ({ remainder: part.remainder, index }));
164
+ order.sort(
165
+ (a, b) => a.remainder > b.remainder ? -1 : a.remainder < b.remainder ? 1 : a.index - b.index
166
+ );
167
+ for (let i = 0; i < leftover; i++) {
168
+ parts[order[i].index].base += 1n;
169
+ }
170
+ return parts.map((part) => _Money.fromMinor(part.base, scale, this.currency));
171
+ }
172
+ /** Verteilung in n gleiche Teile (Sammelposten-Fünftel, AfA-Monatsraten). */
173
+ allocateEvenly(parts) {
174
+ if (parts < 1) {
175
+ throw new InvalidValue("allocateEvenly braucht mindestens einen Teil");
176
+ }
177
+ return this.allocate(...new Array(parts).fill(1));
178
+ }
179
+ /** Betrag als String-Dezimal mit fester Skala, z. B. "1234.56" (datenformat.md). */
180
+ amountAsString() {
181
+ return this.amount.toFixed(this.currency.scale);
182
+ }
183
+ toJSON() {
184
+ return { amount: this.amountAsString(), currency: this.currency.code };
185
+ }
186
+ toString() {
187
+ return `${this.amountAsString()} ${this.currency.code}`;
188
+ }
189
+ static fromMinor(minor, scale, currency) {
190
+ const value = new Big(minor.toString()).div(new Big(10).pow(scale));
191
+ return new _Money(value.round(scale, HALF_UP), currency);
192
+ }
193
+ /** Dezimalgewichte verlustfrei auf ganzzahlige Gewichte gleicher Skala bringen. */
194
+ static normalizeWeights(weights) {
195
+ const decimals = [];
196
+ let maxScale = 0;
197
+ for (const weight of weights) {
198
+ let decimal;
199
+ try {
200
+ decimal = new Big(weight);
201
+ } catch {
202
+ throw new InvalidValue(`Ung\xFCltiges Gewicht "${weight}"`);
203
+ }
204
+ if (decimal.lt(0)) {
205
+ throw new InvalidValue("Gewichte d\xFCrfen nicht negativ sein");
206
+ }
207
+ decimals.push(decimal);
208
+ maxScale = Math.max(maxScale, decimalPlaces(decimal));
209
+ }
210
+ const factor = new Big(10).pow(maxScale);
211
+ return decimals.map((decimal) => BigInt(decimal.times(factor).toFixed(0)));
212
+ }
213
+ assertSameCurrency(other) {
214
+ if (!this.currency.equals(other.currency)) {
215
+ throw new CurrencyMismatch(
216
+ `W\xE4hrungen mischen sich nicht: ${this.currency.code} vs. ${other.currency.code}`
217
+ );
218
+ }
219
+ }
220
+ };
221
+
222
+ // src/shared/canonical-json.ts
223
+ var MAX_SAFE_INTEGER = 9007199254740991;
224
+ function canonicalJson(value) {
225
+ return encodeValue(value);
226
+ }
227
+ function encodeValue(value) {
228
+ if (value === null) {
229
+ return "null";
230
+ }
231
+ if (typeof value === "boolean") {
232
+ return value ? "true" : "false";
233
+ }
234
+ if (typeof value === "number") {
235
+ if (!Number.isInteger(value)) {
236
+ throw new InvalidValue(
237
+ "Floats sind im Datenformat verboten (Betr\xE4ge als String-Dezimal, datenformat.md)"
238
+ );
239
+ }
240
+ if (Math.abs(value) > MAX_SAFE_INTEGER) {
241
+ throw new InvalidValue(`Ganzzahl au\xDFerhalb des sicheren Bereichs (|x| > 2^53-1): ${value}`);
242
+ }
243
+ return String(value);
244
+ }
245
+ if (typeof value === "string") {
246
+ return encodeString(value);
247
+ }
248
+ if (Array.isArray(value)) {
249
+ return `[${value.map(encodeValue).join(",")}]`;
250
+ }
251
+ if (typeof value === "object") {
252
+ const candidate = value;
253
+ if (typeof candidate.toJSON === "function") {
254
+ return encodeValue(candidate.toJSON());
255
+ }
256
+ return encodeObject(value);
257
+ }
258
+ throw new InvalidValue(`Nicht serialisierbarer Typ: ${typeof value}`);
259
+ }
260
+ function encodeObject(object) {
261
+ const keys = Object.keys(object);
262
+ if (keys.length === 0) {
263
+ return "{}";
264
+ }
265
+ keys.sort();
266
+ return `{${keys.map((key) => `${encodeString(key)}:${encodeValue(object[key])}`).join(",")}}`;
267
+ }
268
+ function encodeString(value) {
269
+ let out = '"';
270
+ for (let i = 0; i < value.length; i++) {
271
+ const char = value[i];
272
+ const code = value.charCodeAt(i);
273
+ if (char === '"') {
274
+ out += '\\"';
275
+ } else if (char === "\\") {
276
+ out += "\\\\";
277
+ } else if (code === 8) {
278
+ out += "\\b";
279
+ } else if (code === 9) {
280
+ out += "\\t";
281
+ } else if (code === 10) {
282
+ out += "\\n";
283
+ } else if (code === 12) {
284
+ out += "\\f";
285
+ } else if (code === 13) {
286
+ out += "\\r";
287
+ } else if (code < 32) {
288
+ out += `\\u${code.toString(16).padStart(4, "0")}`;
289
+ } else {
290
+ out += char;
291
+ }
292
+ }
293
+ return `${out}"`;
294
+ }
295
+
296
+ // src/shared/clock.ts
297
+ var SystemClock = class {
298
+ now() {
299
+ return /* @__PURE__ */ new Date();
300
+ }
301
+ };
302
+ var FixedClock = class _FixedClock {
303
+ current;
304
+ constructor(at) {
305
+ this.current = at;
306
+ }
307
+ static at(iso8601) {
308
+ return new _FixedClock(new Date(iso8601));
309
+ }
310
+ now() {
311
+ return this.current;
312
+ }
313
+ advanceMilliseconds(milliseconds) {
314
+ this.current = new Date(this.current.getTime() + milliseconds);
315
+ }
316
+ };
317
+ var PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
318
+ var Uuid = class _Uuid {
319
+ constructor(value) {
320
+ this.value = value;
321
+ }
322
+ value;
323
+ static fromString(value) {
324
+ const normalized = value.toLowerCase();
325
+ if (!PATTERN.test(normalized)) {
326
+ throw new InvalidValue(`Keine g\xFCltige UUID: "${value}"`);
327
+ }
328
+ return new _Uuid(normalized);
329
+ }
330
+ static v7(clock = new SystemClock()) {
331
+ const time = clock.now().getTime().toString(16).padStart(12, "0");
332
+ const random = randomBytes(10).toString("hex");
333
+ const variant = (parseInt(random[3], 16) & 3 | 8).toString(16);
334
+ return new _Uuid(
335
+ `${time.slice(0, 8)}-${time.slice(8, 12)}-7${random.slice(0, 3)}-${variant}${random.slice(4, 7)}-${random.slice(7, 19)}`
336
+ );
337
+ }
338
+ equals(other) {
339
+ return this.value === other.value;
340
+ }
341
+ /** Byteweise = zeitliche Ordnung bei v7. */
342
+ compareTo(other) {
343
+ return this.value < other.value ? -1 : this.value > other.value ? 1 : 0;
344
+ }
345
+ toJSON() {
346
+ return this.value;
347
+ }
348
+ toString() {
349
+ return this.value;
350
+ }
351
+ };
352
+
353
+ // src/shared/id-generator.ts
354
+ var UuidV7IdGenerator = class {
355
+ constructor(clock = new SystemClock()) {
356
+ this.clock = clock;
357
+ }
358
+ clock;
359
+ next() {
360
+ return Uuid.v7(this.clock);
361
+ }
362
+ };
363
+ var DeterministicIdGenerator = class {
364
+ constructor(clock) {
365
+ this.clock = clock;
366
+ }
367
+ clock;
368
+ counter = 0;
369
+ next() {
370
+ this.counter++;
371
+ const time = this.clock.now().getTime().toString(16).padStart(12, "0");
372
+ const sequence = this.counter.toString(16).padStart(18, "0");
373
+ return Uuid.fromString(
374
+ `${time.slice(0, 8)}-${time.slice(8, 12)}-7${sequence.slice(0, 3)}-8${sequence.slice(3, 6)}-${sequence.slice(6, 18)}`
375
+ );
376
+ }
377
+ };
378
+
379
+ // src/shared/calendar-date.ts
380
+ function toIso(year, monthIndex, day) {
381
+ const date = new Date(Date.UTC(year, monthIndex, day));
382
+ const y = String(date.getUTCFullYear()).padStart(4, "0");
383
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
384
+ const d = String(date.getUTCDate()).padStart(2, "0");
385
+ return `${y}-${m}-${d}`;
386
+ }
387
+ var CalendarDate = class _CalendarDate {
388
+ constructor(iso) {
389
+ this.iso = iso;
390
+ }
391
+ iso;
392
+ static of(iso) {
393
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
394
+ throw new InvalidValue(`Kein g\xFCltiges Kalenderdatum: "${iso}"`);
395
+ }
396
+ const year = Number(iso.slice(0, 4));
397
+ const month = Number(iso.slice(5, 7));
398
+ const day = Number(iso.slice(8, 10));
399
+ if (toIso(year, month - 1, day) !== iso) {
400
+ throw new InvalidValue(`Kein g\xFCltiges Kalenderdatum: "${iso}"`);
401
+ }
402
+ return new _CalendarDate(iso);
403
+ }
404
+ compareTo(other) {
405
+ return this.iso < other.iso ? -1 : this.iso > other.iso ? 1 : 0;
406
+ }
407
+ equals(other) {
408
+ return this.iso === other.iso;
409
+ }
410
+ isBefore(other) {
411
+ return this.compareTo(other) < 0;
412
+ }
413
+ isAfter(other) {
414
+ return this.compareTo(other) > 0;
415
+ }
416
+ isBetween(start, end) {
417
+ return !this.isBefore(start) && !this.isAfter(end);
418
+ }
419
+ year() {
420
+ return Number(this.iso.slice(0, 4));
421
+ }
422
+ month() {
423
+ return Number(this.iso.slice(5, 7));
424
+ }
425
+ lastDayOfMonth() {
426
+ return new _CalendarDate(toIso(this.year(), this.month(), 0));
427
+ }
428
+ firstDayOfNextMonth() {
429
+ return new _CalendarDate(toIso(this.year(), this.month(), 1));
430
+ }
431
+ toJSON() {
432
+ return this.iso;
433
+ }
434
+ toString() {
435
+ return this.iso;
436
+ }
437
+ };
438
+
439
+ // src/shared/account-number.ts
440
+ var AccountNumber = class _AccountNumber {
441
+ constructor(value) {
442
+ this.value = value;
443
+ }
444
+ value;
445
+ static of(value) {
446
+ if (value === "" || value.length > 64) {
447
+ throw new InvalidValue("Kontonummer muss 1-64 Zeichen lang sein");
448
+ }
449
+ if (!/^[^\s\p{C}]+$/u.test(value)) {
450
+ throw new InvalidValue(`Kontonummer enth\xE4lt Whitespace oder Steuerzeichen: "${value}"`);
451
+ }
452
+ return new _AccountNumber(value);
453
+ }
454
+ compareTo(other) {
455
+ return this.value < other.value ? -1 : this.value > other.value ? 1 : 0;
456
+ }
457
+ equals(other) {
458
+ return this.value === other.value;
459
+ }
460
+ toJSON() {
461
+ return this.value;
462
+ }
463
+ toString() {
464
+ return this.value;
465
+ }
466
+ };
467
+
468
+ // src/shared/period-ref.ts
469
+ var PeriodRef = class {
470
+ constructor(fiscalYear, period) {
471
+ this.fiscalYear = fiscalYear;
472
+ this.period = period;
473
+ if (fiscalYear < 1 || fiscalYear > 9999) {
474
+ throw new InvalidValue(`Ung\xFCltiges Gesch\xE4ftsjahr: ${fiscalYear}`);
475
+ }
476
+ if (period < 1 || period > 999) {
477
+ throw new InvalidValue(`Ung\xFCltige Periodennummer: ${period}`);
478
+ }
479
+ }
480
+ fiscalYear;
481
+ period;
482
+ /** Chronologisch: erst Jahr, dann Periode. */
483
+ compareTo(other) {
484
+ if (this.fiscalYear !== other.fiscalYear) {
485
+ return this.fiscalYear < other.fiscalYear ? -1 : 1;
486
+ }
487
+ return this.period < other.period ? -1 : this.period > other.period ? 1 : 0;
488
+ }
489
+ equals(other) {
490
+ return this.fiscalYear === other.fiscalYear && this.period === other.period;
491
+ }
492
+ toJSON() {
493
+ return { fiscalYear: this.fiscalYear, period: this.period };
494
+ }
495
+ };
496
+
497
+ // src/shared/dimension-value.ts
498
+ var DimensionValue = class _DimensionValue {
499
+ constructor(type, code) {
500
+ this.type = type;
501
+ this.code = code;
502
+ }
503
+ type;
504
+ code;
505
+ static of(type, code) {
506
+ if (type === "" || code === "") {
507
+ throw new InvalidValue("Dimensionstyp und -code d\xFCrfen nicht leer sein");
508
+ }
509
+ return new _DimensionValue(type, code);
510
+ }
511
+ equals(other) {
512
+ return this.type === other.type && this.code === other.code;
513
+ }
514
+ toJSON() {
515
+ return { type: this.type, code: this.code };
516
+ }
517
+ };
518
+
519
+ // src/domain-error.ts
520
+ var DomainError = class extends Error {
521
+ constructor(errorCode, message = "", details = {}) {
522
+ super(message !== "" ? message : errorCode);
523
+ this.errorCode = errorCode;
524
+ this.details = details;
525
+ this.name = "DomainError";
526
+ }
527
+ errorCode;
528
+ details;
529
+ };
530
+
531
+ // src/ledger/types.ts
532
+ var BALANCE_CARRYING = /* @__PURE__ */ new Set([
533
+ "asset",
534
+ "liability",
535
+ "equity"
536
+ ]);
537
+ function isBalanceCarrying(type) {
538
+ return BALANCE_CARRYING.has(type);
539
+ }
540
+ function isAccountType(value) {
541
+ return value === "asset" || value === "liability" || value === "equity" || value === "expense" || value === "revenue";
542
+ }
543
+ function parseSettlementDifferenceKind(value) {
544
+ return value === "discount" || value === "bad_debt" || value === "minor" ? value : null;
545
+ }
546
+ function parseOpenItemKind(value) {
547
+ return value === "receivable" || value === "payable" ? value : null;
548
+ }
549
+
550
+ // src/ledger/account.ts
551
+ var Account = class {
552
+ constructor(id, number, name, type, subtype, status = "active") {
553
+ this.id = id;
554
+ this.number = number;
555
+ this.name = name;
556
+ this.type = type;
557
+ this.subtype = subtype;
558
+ this.accountStatus = status;
559
+ }
560
+ id;
561
+ number;
562
+ name;
563
+ type;
564
+ subtype;
565
+ accountStatus;
566
+ status() {
567
+ return this.accountStatus;
568
+ }
569
+ isLocked() {
570
+ return this.accountStatus === "locked";
571
+ }
572
+ lock() {
573
+ this.accountStatus = "locked";
574
+ }
575
+ toJSON() {
576
+ return {
577
+ id: this.id.value,
578
+ number: this.number.value,
579
+ name: this.name,
580
+ type: this.type,
581
+ subtype: this.subtype,
582
+ status: this.accountStatus
583
+ };
584
+ }
585
+ };
586
+
587
+ // src/ledger/entry-line.ts
588
+ var EntryLine = class _EntryLine {
589
+ constructor(accountId, account, side, money, dimensions = [], taxTag = null) {
590
+ this.accountId = accountId;
591
+ this.account = account;
592
+ this.side = side;
593
+ this.money = money;
594
+ this.dimensions = dimensions;
595
+ this.taxTag = taxTag;
596
+ }
597
+ accountId;
598
+ account;
599
+ side;
600
+ money;
601
+ dimensions;
602
+ taxTag;
603
+ negated() {
604
+ return new _EntryLine(
605
+ this.accountId,
606
+ this.account,
607
+ this.side,
608
+ this.money.negate(),
609
+ this.dimensions,
610
+ this.taxTag
611
+ );
612
+ }
613
+ toJSON() {
614
+ return {
615
+ accountId: this.accountId.value,
616
+ account: this.account.value,
617
+ side: this.side,
618
+ money: this.money.toJSON(),
619
+ dimensions: this.dimensions.map((dimension) => dimension.toJSON()),
620
+ taxTag: this.taxTag
621
+ };
622
+ }
623
+ };
624
+
625
+ // src/ledger/journal-entry.ts
626
+ var JournalEntry = class {
627
+ constructor(id, sequenceNumber, entryDate, voucherDate, recordedAt, periodRef, voucherId, text, lines, reverses = null, reversedBy = null, status = "entered") {
628
+ this.id = id;
629
+ this.sequenceNumber = sequenceNumber;
630
+ this.entryDate = entryDate;
631
+ this.voucherDate = voucherDate;
632
+ this.recordedAt = recordedAt;
633
+ this.periodRef = periodRef;
634
+ this.voucherId = voucherId;
635
+ this.reverses = reverses;
636
+ this.entryText = text;
637
+ this.entryLines = lines;
638
+ this.entryReversedBy = reversedBy;
639
+ this.entryStatus = status;
640
+ }
641
+ id;
642
+ sequenceNumber;
643
+ entryDate;
644
+ voucherDate;
645
+ recordedAt;
646
+ periodRef;
647
+ voucherId;
648
+ reverses;
649
+ entryText;
650
+ entryLines;
651
+ entryReversedBy;
652
+ entryStatus;
653
+ status() {
654
+ return this.entryStatus;
655
+ }
656
+ isFinalized() {
657
+ return this.entryStatus === "finalized";
658
+ }
659
+ text() {
660
+ return this.entryText;
661
+ }
662
+ lines() {
663
+ return this.entryLines;
664
+ }
665
+ reversedBy() {
666
+ return this.entryReversedBy;
667
+ }
668
+ changeText(text) {
669
+ this.assertCorrectable();
670
+ this.entryText = text;
671
+ }
672
+ changeLines(lines) {
673
+ this.assertCorrectable();
674
+ this.entryLines = lines;
675
+ }
676
+ finalize() {
677
+ this.entryStatus = "finalized";
678
+ }
679
+ markReversed(reversalId) {
680
+ if (this.entryReversedBy !== null) {
681
+ throw new DomainError(
682
+ "E_ENTRY_ALREADY_REVERSED",
683
+ `Buchung ${this.id.value} ist bereits storniert (durch ${this.entryReversedBy.value})`,
684
+ { entryId: this.id.value, reversedBy: this.entryReversedBy.value }
685
+ );
686
+ }
687
+ this.entryReversedBy = reversalId;
688
+ }
689
+ assertCorrectable() {
690
+ if (this.entryStatus !== "entered") {
691
+ throw new DomainError(
692
+ "E_ENTRY_FINALIZED",
693
+ `Buchung ${this.id.value} ist festgeschrieben \u2014 Korrektur nicht m\xF6glich, nur Storno`,
694
+ { entryId: this.id.value }
695
+ );
696
+ }
697
+ }
698
+ toJSON() {
699
+ return {
700
+ id: this.id.value,
701
+ sequenceNumber: this.sequenceNumber,
702
+ status: this.entryStatus,
703
+ entryDate: this.entryDate.iso,
704
+ voucherDate: this.voucherDate?.iso ?? null,
705
+ recordedAt: this.recordedAt,
706
+ periodRef: this.periodRef.toJSON(),
707
+ voucherId: this.voucherId.value,
708
+ text: this.entryText,
709
+ lines: this.entryLines.map((line) => line.toJSON()),
710
+ reverses: this.reverses?.value ?? null,
711
+ reversedBy: this.entryReversedBy?.value ?? null
712
+ };
713
+ }
714
+ };
715
+
716
+ // src/ledger/voucher.ts
717
+ var Voucher = class {
718
+ id;
719
+ voucherNumber;
720
+ voucherDate;
721
+ due;
722
+ recurring;
723
+ economicYear;
724
+ supplierTaxationMethod;
725
+ serviceDate;
726
+ servicePeriodFrom;
727
+ servicePeriodTo;
728
+ kind;
729
+ partnerId;
730
+ issuer;
731
+ constructor(props) {
732
+ this.id = props.id;
733
+ this.voucherNumber = props.voucherNumber;
734
+ this.voucherDate = props.voucherDate;
735
+ this.due = props.due ?? null;
736
+ this.recurring = props.recurring ?? false;
737
+ this.economicYear = props.economicYear ?? null;
738
+ this.supplierTaxationMethod = props.supplierTaxationMethod ?? null;
739
+ this.serviceDate = props.serviceDate ?? null;
740
+ this.servicePeriodFrom = props.servicePeriodFrom ?? null;
741
+ this.servicePeriodTo = props.servicePeriodTo ?? null;
742
+ this.kind = props.kind ?? null;
743
+ this.partnerId = props.partnerId ?? null;
744
+ this.issuer = props.issuer ?? null;
745
+ }
746
+ /** Steuerlich maßgebliches Datum: Leistungsdatum, Fallback Belegdatum. */
747
+ taxDate() {
748
+ return this.serviceDate ?? this.servicePeriodTo ?? this.voucherDate;
749
+ }
750
+ toJSON() {
751
+ return {
752
+ id: this.id.value,
753
+ voucherNumber: this.voucherNumber,
754
+ voucherDate: this.voucherDate.iso,
755
+ due: this.due?.iso ?? null,
756
+ recurring: this.recurring,
757
+ economicYear: this.economicYear,
758
+ supplierTaxationMethod: this.supplierTaxationMethod,
759
+ serviceDate: this.serviceDate?.iso ?? null,
760
+ servicePeriod: this.servicePeriodFrom === null ? null : { from: this.servicePeriodFrom.iso, to: this.servicePeriodTo?.iso ?? null },
761
+ kind: this.kind,
762
+ partnerId: this.partnerId?.value ?? null,
763
+ issuer: this.issuer
764
+ };
765
+ }
766
+ };
767
+
768
+ // src/ledger/period.ts
769
+ var Period = class {
770
+ constructor(number, start, end, status = "open") {
771
+ this.number = number;
772
+ this.start = start;
773
+ this.end = end;
774
+ this.periodStatus = status;
775
+ }
776
+ number;
777
+ start;
778
+ end;
779
+ periodStatus;
780
+ status() {
781
+ return this.periodStatus;
782
+ }
783
+ isOpen() {
784
+ return this.periodStatus === "open";
785
+ }
786
+ contains(date) {
787
+ return date.isBetween(this.start, this.end);
788
+ }
789
+ /** Nur über FiscalYear aufrufen (Reihenfolgeprüfung dort). */
790
+ close() {
791
+ this.periodStatus = "closed";
792
+ }
793
+ /** Nur über FiscalYear aufrufen (Jahresstatusprüfung dort). */
794
+ reopen() {
795
+ this.periodStatus = "open";
796
+ }
797
+ };
798
+
799
+ // src/ledger/fiscal-year.ts
800
+ var FiscalYear = class _FiscalYear {
801
+ constructor(id, year, start, end, periodList, status = "open") {
802
+ this.id = id;
803
+ this.year = year;
804
+ this.start = start;
805
+ this.end = end;
806
+ this.periodList = periodList;
807
+ this.fiscalStatus = status;
808
+ }
809
+ id;
810
+ year;
811
+ start;
812
+ end;
813
+ periodList;
814
+ fiscalStatus;
815
+ static create(id, year, start, end, explicitPeriods = null) {
816
+ if (!start.isBefore(end)) {
817
+ throw new InvalidValue("Gesch\xE4ftsjahr: start muss vor end liegen");
818
+ }
819
+ const periods = explicitPeriods === null ? _FiscalYear.monthlyPeriods(start, end) : explicitPeriods.map((d) => new Period(d.period, d.start, d.end));
820
+ return new _FiscalYear(id, year, start, end, periods);
821
+ }
822
+ static monthlyPeriods(start, end) {
823
+ const periods = [];
824
+ let cursor = start;
825
+ let number = 1;
826
+ while (!cursor.isAfter(end)) {
827
+ const monthEnd = cursor.lastDayOfMonth();
828
+ const periodEnd = monthEnd.isAfter(end) ? end : monthEnd;
829
+ periods.push(new Period(number, cursor, periodEnd));
830
+ cursor = cursor.firstDayOfNextMonth();
831
+ number++;
832
+ }
833
+ return periods;
834
+ }
835
+ status() {
836
+ return this.fiscalStatus;
837
+ }
838
+ isClosed() {
839
+ return this.fiscalStatus === "closed";
840
+ }
841
+ periods() {
842
+ return this.periodList;
843
+ }
844
+ period(number) {
845
+ const found = this.periodList.find((p) => p.number === number);
846
+ if (found === void 0) {
847
+ throw new DomainError(
848
+ "E_PERIOD_UNKNOWN",
849
+ `Periode ${number} existiert nicht im Gesch\xE4ftsjahr ${this.year}`,
850
+ { fiscalYear: this.year, period: number }
851
+ );
852
+ }
853
+ return found;
854
+ }
855
+ contains(date) {
856
+ return date.isBetween(this.start, this.end);
857
+ }
858
+ periodForDate(date) {
859
+ const found = this.periodList.find((p) => p.contains(date));
860
+ if (found === void 0) {
861
+ throw new DomainError(
862
+ "E_PERIOD_UNKNOWN",
863
+ `Kein Periodenzeitraum f\xFCr ${date.iso} im Gesch\xE4ftsjahr ${this.year}`,
864
+ { date: date.iso, fiscalYear: this.year }
865
+ );
866
+ }
867
+ return found;
868
+ }
869
+ closePeriod(number) {
870
+ this.assertNotClosed();
871
+ const target = this.period(number);
872
+ for (const period of this.periodList) {
873
+ if (period.number < number && period.isOpen()) {
874
+ throw new DomainError(
875
+ "E_PERIOD_OUT_OF_ORDER",
876
+ `Periode ${number} kann nicht geschlossen werden: Periode ${period.number} ist noch offen`,
877
+ { fiscalYear: this.year, period: number, openPeriod: period.number }
878
+ );
879
+ }
880
+ }
881
+ target.close();
882
+ return target;
883
+ }
884
+ reopenPeriod(number) {
885
+ this.assertNotClosed();
886
+ const target = this.period(number);
887
+ target.reopen();
888
+ return target;
889
+ }
890
+ /** Reiner Statuswechsel — keine fachliche Buchungswirkung (api.md v0.3). */
891
+ close() {
892
+ for (const period of this.periodList) {
893
+ if (period.isOpen()) {
894
+ throw new DomainError(
895
+ "E_PERIOD_OUT_OF_ORDER",
896
+ `Jahresabschluss ${this.year}: Periode ${period.number} ist noch offen`,
897
+ { fiscalYear: this.year, openPeriod: period.number }
898
+ );
899
+ }
900
+ }
901
+ this.fiscalStatus = "closed";
902
+ }
903
+ assertNotClosed() {
904
+ if (this.isClosed()) {
905
+ throw new DomainError("E_FISCALYEAR_CLOSED", `Gesch\xE4ftsjahr ${this.year} ist abgeschlossen`, {
906
+ fiscalYear: this.year
907
+ });
908
+ }
909
+ }
910
+ };
911
+
912
+ // src/ledger/audit-record.ts
913
+ var AuditRecord = class {
914
+ constructor(id, at, actor, objectType, objectId, action, changes = {}) {
915
+ this.id = id;
916
+ this.at = at;
917
+ this.actor = actor;
918
+ this.objectType = objectType;
919
+ this.objectId = objectId;
920
+ this.action = action;
921
+ this.changes = changes;
922
+ }
923
+ id;
924
+ at;
925
+ actor;
926
+ objectType;
927
+ objectId;
928
+ action;
929
+ changes;
930
+ toJSON() {
931
+ return {
932
+ id: this.id.value,
933
+ at: this.at,
934
+ actor: this.actor,
935
+ objectType: this.objectType,
936
+ objectId: this.objectId.value,
937
+ action: this.action,
938
+ // Leerer Diff → {} (nicht []), damit das Format stabil bleibt.
939
+ changes: this.changes
940
+ };
941
+ }
942
+ };
943
+
944
+ // src/ledger/open-item.ts
945
+ var OpenItem = class {
946
+ constructor(id, kind, originEntryId, originLineIndex, money, voucherId, openedAt, partnerId = null) {
947
+ this.id = id;
948
+ this.kind = kind;
949
+ this.originEntryId = originEntryId;
950
+ this.originLineIndex = originLineIndex;
951
+ this.money = money;
952
+ this.voucherId = voucherId;
953
+ this.openedAt = openedAt;
954
+ this.partnerId = partnerId;
955
+ }
956
+ id;
957
+ kind;
958
+ originEntryId;
959
+ originLineIndex;
960
+ money;
961
+ voucherId;
962
+ openedAt;
963
+ partnerId;
964
+ settlementList = [];
965
+ settlements() {
966
+ return this.settlementList;
967
+ }
968
+ remaining() {
969
+ return this.remainingAt(null);
970
+ }
971
+ /** Restbetrag zum Stichtag (null = heute/alles). */
972
+ remainingAt(asOf) {
973
+ let remaining = this.money;
974
+ for (const settlement of this.settlementList) {
975
+ if (asOf !== null && settlement.settledAt.isAfter(asOf)) continue;
976
+ remaining = remaining.subtract(settlement.money);
977
+ }
978
+ return remaining;
979
+ }
980
+ status() {
981
+ return this.statusAt(null);
982
+ }
983
+ statusAt(asOf) {
984
+ const remaining = this.remainingAt(asOf);
985
+ if (remaining.isZero()) return "settled";
986
+ return remaining.equals(this.money) ? "open" : "partially_settled";
987
+ }
988
+ settle(settlement) {
989
+ if (settlement.money.compareTo(this.remaining()) > 0) {
990
+ throw new DomainError(
991
+ "E_SETTLEMENT_EXCEEDS_ITEM",
992
+ `Zuordnung ${settlement.money.amountAsString()} \xFCbersteigt Restbetrag ${this.remaining().amountAsString()} des Postens ${this.id.value}`,
993
+ {
994
+ openItemId: this.id.value,
995
+ remaining: this.remaining().amountAsString(),
996
+ allocated: settlement.money.amountAsString()
997
+ }
998
+ );
999
+ }
1000
+ this.settlementList.push(settlement);
1001
+ }
1002
+ toJSON() {
1003
+ return {
1004
+ id: this.id.value,
1005
+ kind: this.kind,
1006
+ originEntryId: this.originEntryId.value,
1007
+ originLineIndex: this.originLineIndex,
1008
+ money: this.money.toJSON(),
1009
+ partnerId: this.partnerId?.value ?? null,
1010
+ remaining: this.remaining().toJSON(),
1011
+ status: this.status(),
1012
+ settlements: this.settlementList.map((settlement) => settlement.toJSON())
1013
+ };
1014
+ }
1015
+ };
1016
+
1017
+ // src/ledger/settlement.ts
1018
+ var Settlement = class {
1019
+ constructor(entryId, money, settledAt, differenceMoney = null, differenceKind = null) {
1020
+ this.entryId = entryId;
1021
+ this.money = money;
1022
+ this.settledAt = settledAt;
1023
+ this.differenceMoney = differenceMoney;
1024
+ this.differenceKind = differenceKind;
1025
+ }
1026
+ entryId;
1027
+ money;
1028
+ settledAt;
1029
+ differenceMoney;
1030
+ differenceKind;
1031
+ toJSON() {
1032
+ return {
1033
+ entryId: this.entryId.value,
1034
+ money: this.money.toJSON(),
1035
+ settledAt: this.settledAt.iso,
1036
+ difference: this.differenceMoney === null ? null : { money: this.differenceMoney.toJSON(), kind: this.differenceKind }
1037
+ };
1038
+ }
1039
+ };
1040
+
1041
+ // src/ledger/post-result.ts
1042
+ var PostResult = class {
1043
+ constructor(entry, openItemsCreated) {
1044
+ this.entry = entry;
1045
+ this.openItemsCreated = openItemsCreated;
1046
+ }
1047
+ entry;
1048
+ openItemsCreated;
1049
+ };
1050
+
1051
+ // src/ledger/dimension-registry.ts
1052
+ var DimensionRegistry = class _DimensionRegistry {
1053
+ constructor(types, values, rules) {
1054
+ this.types = types;
1055
+ this.values = values;
1056
+ this.rules = rules;
1057
+ }
1058
+ types;
1059
+ values;
1060
+ rules;
1061
+ static empty() {
1062
+ return new _DimensionRegistry(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), []);
1063
+ }
1064
+ static fromData(dimensionTypes, dimensionValues, dimensionRules) {
1065
+ const types = new Set(dimensionTypes.map((t) => t.code));
1066
+ const values = new Set(dimensionValues.map((v) => `${v.typeCode}:${v.code}`));
1067
+ const rules = dimensionRules.map((r) => ({
1068
+ from: r.accountRange.from,
1069
+ to: r.accountRange.to,
1070
+ required: r.requiredDimension
1071
+ }));
1072
+ return new _DimensionRegistry(types, values, rules);
1073
+ }
1074
+ validateLine(account, dimensions) {
1075
+ for (const dimension of dimensions) {
1076
+ if (!this.types.has(dimension.type)) {
1077
+ throw new DomainError("E_DIMENSION_INVALID", `Unbekannter Dimensionstyp "${dimension.type}"`, {
1078
+ type: dimension.type
1079
+ });
1080
+ }
1081
+ if (!this.values.has(`${dimension.type}:${dimension.code}`)) {
1082
+ throw new DomainError(
1083
+ "E_DIMENSION_INVALID",
1084
+ `Unbekannter Dimensionswert "${dimension.code}" f\xFCr Typ "${dimension.type}"`,
1085
+ { type: dimension.type, code: dimension.code }
1086
+ );
1087
+ }
1088
+ }
1089
+ for (const rule of this.rules) {
1090
+ const inRange = account.value >= rule.from && account.value <= rule.to;
1091
+ if (!inRange) continue;
1092
+ if (dimensions.some((d) => d.type === rule.required)) continue;
1093
+ throw new DomainError(
1094
+ "E_DIMENSION_INVALID",
1095
+ `Pflichtdimension "${rule.required}" fehlt auf Konto ${account.value}`,
1096
+ { account: account.value, required: rule.required }
1097
+ );
1098
+ }
1099
+ }
1100
+ };
1101
+
1102
+ // src/ledger/ledger.ts
1103
+ function isRecord(value) {
1104
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1105
+ }
1106
+ function asString(value) {
1107
+ return typeof value === "string" ? value : null;
1108
+ }
1109
+ var Ledger = class {
1110
+ constructor(baseCurrency, accounts, fiscalYears, vouchers, journal, openItems, audit, dimensions, clock, ids) {
1111
+ this.baseCurrency = baseCurrency;
1112
+ this.accounts = accounts;
1113
+ this.fiscalYears = fiscalYears;
1114
+ this.vouchers = vouchers;
1115
+ this.journal = journal;
1116
+ this.openItems = openItems;
1117
+ this.audit = audit;
1118
+ this.dimensions = dimensions;
1119
+ this.clock = clock;
1120
+ this.ids = ids;
1121
+ }
1122
+ baseCurrency;
1123
+ accounts;
1124
+ fiscalYears;
1125
+ vouchers;
1126
+ journal;
1127
+ openItems;
1128
+ audit;
1129
+ dimensions;
1130
+ clock;
1131
+ ids;
1132
+ post(input) {
1133
+ const actor = this.actor(input);
1134
+ const rawLines = input.lines;
1135
+ if (!Array.isArray(rawLines) || rawLines.length < 2) {
1136
+ throw new DomainError("E_ENTRY_TOO_FEW_LINES", "Eine Buchung braucht mindestens zwei Positionen");
1137
+ }
1138
+ const parsed = rawLines.map((rawLine, index) => {
1139
+ if (!isRecord(rawLine)) {
1140
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Position ${index} ist keine Struktur`);
1141
+ }
1142
+ return this.parseLine(rawLine, index);
1143
+ });
1144
+ const voucher = this.requireVoucher(input.voucherId);
1145
+ const lines = this.resolveLines(parsed);
1146
+ this.assertBalanced(lines);
1147
+ const entryDate = this.parseEntryDate(input.entryDate);
1148
+ const [fiscalYear, period] = this.openPeriodFor(entryDate);
1149
+ const text = asString(input.text) ?? "";
1150
+ const entry = new JournalEntry(
1151
+ this.ids.next(),
1152
+ this.journal.nextSequenceNumber(fiscalYear.year),
1153
+ entryDate,
1154
+ voucher.voucherDate,
1155
+ this.now(),
1156
+ new PeriodRef(fiscalYear.year, period.number),
1157
+ voucher.id,
1158
+ text,
1159
+ lines
1160
+ );
1161
+ this.journal.append(entry);
1162
+ this.recordAudit(actor, "journalEntry", entry.id, "created");
1163
+ return new PostResult(entry, this.createOpenItems(entry));
1164
+ }
1165
+ /**
1166
+ * AR/AP-Automatik: Soll auf Forderungskonto → receivable, Haben auf
1167
+ * Verbindlichkeitskonto → payable. Stornobuchungen erzeugen keine Posten.
1168
+ */
1169
+ createOpenItems(entry) {
1170
+ if (entry.reverses !== null) return [];
1171
+ const created = [];
1172
+ const voucher = this.vouchers.byId(entry.voucherId);
1173
+ entry.lines().forEach((line, index) => {
1174
+ const account = this.accounts.byId(line.accountId);
1175
+ let kind = null;
1176
+ if (account?.subtype === "ar" && line.side === "debit") kind = "receivable";
1177
+ else if (account?.subtype === "ap" && line.side === "credit") kind = "payable";
1178
+ if (kind === null) return;
1179
+ const item = new OpenItem(
1180
+ this.ids.next(),
1181
+ kind,
1182
+ entry.id,
1183
+ index,
1184
+ line.money,
1185
+ entry.voucherId,
1186
+ entry.entryDate,
1187
+ voucher?.partnerId ?? null
1188
+ );
1189
+ this.openItems.add(item);
1190
+ created.push(item);
1191
+ });
1192
+ return created;
1193
+ }
1194
+ /**
1195
+ * Ausgleich: Zuordnung Zahlung → offene(r) Posten, auch teilweise; immer
1196
+ * explizit, kein FIFO (determinismus.md §3). Differenzen (Skonto/Ausfall/
1197
+ * Kleindifferenz) nach api.md G2. Erst vollständig validieren, dann anwenden.
1198
+ */
1199
+ settle(input) {
1200
+ const actor = this.actor(input);
1201
+ const entry = this.requireEntry(input.entryId);
1202
+ const allocations = Array.isArray(input.allocations) ? input.allocations : [];
1203
+ if (allocations.length === 0) {
1204
+ throw new DomainError("E_OPENITEM_UNKNOWN", "settle ohne Zuordnungen");
1205
+ }
1206
+ const plan = [];
1207
+ const planned = /* @__PURE__ */ new Map();
1208
+ for (const allocation of allocations) {
1209
+ if (!isRecord(allocation)) {
1210
+ throw new DomainError("E_OPENITEM_UNKNOWN", "Zuordnung ist keine Struktur");
1211
+ }
1212
+ const openItemId = allocation.openItemId;
1213
+ let item = null;
1214
+ if (typeof openItemId === "string") {
1215
+ try {
1216
+ item = this.openItems.byId(Uuid.fromString(openItemId));
1217
+ } catch (error) {
1218
+ if (!(error instanceof InvalidValue)) throw error;
1219
+ }
1220
+ }
1221
+ if (item === null) {
1222
+ throw new DomainError(
1223
+ "E_OPENITEM_UNKNOWN",
1224
+ `Offener Posten ${typeof openItemId === "string" ? openItemId : "?"} existiert nicht`
1225
+ );
1226
+ }
1227
+ const money = this.parseSettlementMoney(allocation.money, "Zuordnungsbetrag");
1228
+ const [differenceMoney, differenceKind] = this.parseDifference(allocation.difference ?? null, item);
1229
+ const alreadyPlanned = planned.get(item.id.value) ?? Money.zero(this.baseCurrency);
1230
+ if (money.add(alreadyPlanned).compareTo(item.remaining()) > 0) {
1231
+ throw new DomainError(
1232
+ "E_SETTLEMENT_EXCEEDS_ITEM",
1233
+ `Zuordnung ${money.amountAsString()} \xFCbersteigt Restbetrag ${item.remaining().subtract(alreadyPlanned).amountAsString()} des Postens ${item.id.value}`,
1234
+ { openItemId: item.id.value }
1235
+ );
1236
+ }
1237
+ planned.set(item.id.value, money.add(alreadyPlanned));
1238
+ plan.push({
1239
+ item,
1240
+ settlement: new Settlement(entry.id, money, entry.entryDate, differenceMoney, differenceKind)
1241
+ });
1242
+ }
1243
+ const affected = [];
1244
+ for (const step of plan) {
1245
+ const before = step.item.remaining().amountAsString();
1246
+ step.item.settle(step.settlement);
1247
+ this.openItems.save(step.item);
1248
+ this.recordAudit(actor, "openItem", step.item.id, "settled", {
1249
+ remaining: { from: before, to: step.item.remaining().amountAsString() }
1250
+ });
1251
+ affected.push(step.item);
1252
+ }
1253
+ return affected;
1254
+ }
1255
+ parseSettlementMoney(raw, label) {
1256
+ const amount = isRecord(raw) ? asString(raw.amount) : null;
1257
+ const currency = isRecord(raw) ? asString(raw.currency) : null;
1258
+ if (amount === null || currency !== this.baseCurrency.code) {
1259
+ throw new InvalidValue(`${label} fehlt oder falsche W\xE4hrung`);
1260
+ }
1261
+ const money = Money.of(amount, this.baseCurrency);
1262
+ if (!money.isPositive()) {
1263
+ throw new InvalidValue(`${label} muss > 0 sein`);
1264
+ }
1265
+ return money;
1266
+ }
1267
+ parseDifference(raw, item) {
1268
+ if (raw === null) return [null, null];
1269
+ if (!isRecord(raw)) {
1270
+ throw new DomainError("E_SETTLEMENT_DIFFERENCE_INVALID", "difference ist keine Struktur");
1271
+ }
1272
+ const kind = parseSettlementDifferenceKind(raw.kind);
1273
+ if (kind === null) {
1274
+ throw new DomainError(
1275
+ "E_SETTLEMENT_DIFFERENCE_INVALID",
1276
+ `Unbekannte Differenzart "${typeof raw.kind === "string" ? raw.kind : "?"}"`
1277
+ );
1278
+ }
1279
+ let money;
1280
+ try {
1281
+ money = this.parseSettlementMoney(raw.money, "Differenzbetrag");
1282
+ } catch (error) {
1283
+ if (error instanceof InvalidValue) {
1284
+ throw new DomainError("E_SETTLEMENT_DIFFERENCE_INVALID", "Differenzbetrag ung\xFCltig (\u2264 0 oder Format)");
1285
+ }
1286
+ throw error;
1287
+ }
1288
+ if (money.compareTo(item.remaining()) > 0) {
1289
+ throw new DomainError(
1290
+ "E_SETTLEMENT_DIFFERENCE_INVALID",
1291
+ `Differenz ${money.amountAsString()} \xFCbersteigt Restbetrag ${item.remaining().amountAsString()}`
1292
+ );
1293
+ }
1294
+ return [money, kind];
1295
+ }
1296
+ correct(input) {
1297
+ const actor = this.actor(input);
1298
+ const entry = this.requireEntry(input.entryId);
1299
+ const changes = {};
1300
+ const text = asString(input.text);
1301
+ if (text !== null && text !== entry.text()) {
1302
+ changes.text = { from: entry.text(), to: text };
1303
+ entry.changeText(text);
1304
+ }
1305
+ if (Array.isArray(input.lines)) {
1306
+ if (input.lines.length < 2) {
1307
+ throw new DomainError("E_ENTRY_TOO_FEW_LINES", "Eine Buchung braucht mindestens zwei Positionen");
1308
+ }
1309
+ const parsed = input.lines.map((rawLine, index) => {
1310
+ if (!isRecord(rawLine)) {
1311
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Position ${index} ist keine Struktur`);
1312
+ }
1313
+ return this.parseLine(rawLine, index);
1314
+ });
1315
+ const lines = this.resolveLines(parsed);
1316
+ this.assertBalanced(lines);
1317
+ changes.lines = {
1318
+ from: entry.lines().map((line) => line.toJSON()),
1319
+ to: lines.map((line) => line.toJSON())
1320
+ };
1321
+ entry.changeLines(lines);
1322
+ }
1323
+ if (Object.keys(changes).length > 0) {
1324
+ this.journal.save(entry);
1325
+ this.recordAudit(actor, "journalEntry", entry.id, "corrected", changes);
1326
+ } else {
1327
+ entry.changeText(entry.text());
1328
+ }
1329
+ return entry;
1330
+ }
1331
+ finalize(input) {
1332
+ const actor = this.actor(input);
1333
+ if (input.entryId !== void 0) {
1334
+ const entry = this.requireEntry(input.entryId);
1335
+ if (entry.isFinalized()) return 0;
1336
+ entry.finalize();
1337
+ this.journal.save(entry);
1338
+ this.recordAudit(actor, "journalEntry", entry.id, "finalized", {
1339
+ status: { from: "entered", to: "finalized" }
1340
+ });
1341
+ return 1;
1342
+ }
1343
+ const until = input.finalizeUntil;
1344
+ if (typeof until !== "string") {
1345
+ throw new DomainError("E_ENTRY_UNKNOWN", "finalize braucht entryId oder finalizeUntil");
1346
+ }
1347
+ const untilDate = this.parseEntryDate(until);
1348
+ let count = 0;
1349
+ for (const entry of this.journal.all()) {
1350
+ if (entry.isFinalized() || entry.entryDate.isAfter(untilDate)) continue;
1351
+ entry.finalize();
1352
+ this.journal.save(entry);
1353
+ this.recordAudit(actor, "journalEntry", entry.id, "finalized", {
1354
+ status: { from: "entered", to: "finalized" }
1355
+ });
1356
+ count++;
1357
+ }
1358
+ return count;
1359
+ }
1360
+ reverse(input) {
1361
+ const actor = this.actor(input);
1362
+ const original = this.requireEntry(input.entryId);
1363
+ if (original.reversedBy() !== null) {
1364
+ throw new DomainError("E_ENTRY_ALREADY_REVERSED", `Buchung ${original.id.value} ist bereits storniert`, {
1365
+ entryId: original.id.value
1366
+ });
1367
+ }
1368
+ const entryDate = this.parseEntryDate(input.entryDate);
1369
+ const [fiscalYear, period] = this.openPeriodFor(entryDate);
1370
+ const text = asString(input.text) ?? `Storno ${original.sequenceNumber}`;
1371
+ const reversal = new JournalEntry(
1372
+ this.ids.next(),
1373
+ this.journal.nextSequenceNumber(fiscalYear.year),
1374
+ entryDate,
1375
+ original.voucherDate,
1376
+ this.now(),
1377
+ new PeriodRef(fiscalYear.year, period.number),
1378
+ original.voucherId,
1379
+ text,
1380
+ original.lines().map((line) => line.negated()),
1381
+ original.id
1382
+ );
1383
+ original.markReversed(reversal.id);
1384
+ this.journal.append(reversal);
1385
+ this.journal.save(original);
1386
+ this.recordAudit(actor, "journalEntry", reversal.id, "created");
1387
+ this.recordAudit(actor, "journalEntry", original.id, "reversed", {
1388
+ reversedBy: { from: null, to: reversal.id.value }
1389
+ });
1390
+ return reversal;
1391
+ }
1392
+ closePeriod(input) {
1393
+ const fiscalYear = this.requireFiscalYear(input.fiscalYear);
1394
+ const period = fiscalYear.closePeriod(this.periodNumber(input));
1395
+ this.fiscalYears.save(fiscalYear);
1396
+ return { fiscalYear: fiscalYear.year, period: period.number, status: period.status() };
1397
+ }
1398
+ reopenPeriod(input) {
1399
+ const fiscalYear = this.requireFiscalYear(input.fiscalYear);
1400
+ const period = fiscalYear.reopenPeriod(this.periodNumber(input));
1401
+ this.fiscalYears.save(fiscalYear);
1402
+ return { fiscalYear: fiscalYear.year, period: period.number, status: period.status() };
1403
+ }
1404
+ closeFiscalYear(input) {
1405
+ const fiscalYear = this.requireFiscalYear(input.fiscalYear);
1406
+ for (const entry of this.journal.forFiscalYear(fiscalYear.year)) {
1407
+ if (!entry.isFinalized()) {
1408
+ throw new DomainError(
1409
+ "E_FISCALYEAR_UNFINALIZED_ENTRIES",
1410
+ `Jahresabschluss ${fiscalYear.year}: Buchung ${entry.sequenceNumber} ist nicht festgeschrieben`,
1411
+ { fiscalYear: fiscalYear.year, sequenceNumber: entry.sequenceNumber }
1412
+ );
1413
+ }
1414
+ }
1415
+ fiscalYear.close();
1416
+ this.fiscalYears.save(fiscalYear);
1417
+ return fiscalYear;
1418
+ }
1419
+ createFiscalYear(input) {
1420
+ const year = typeof input.year === "number" ? input.year : 0;
1421
+ const start = this.parseEntryDate(input.start);
1422
+ const end = this.parseEntryDate(input.end);
1423
+ for (const existing of this.fiscalYears.all()) {
1424
+ const overlaps = !existing.end.isBefore(start) && !existing.start.isAfter(end);
1425
+ if (overlaps || existing.year === year) {
1426
+ throw new DomainError(
1427
+ "E_FISCALYEAR_OVERLAP",
1428
+ `Gesch\xE4ftsjahr ${year} (${start.iso} bis ${end.iso}) \xFCberschneidet sich mit ${existing.year}`,
1429
+ { year, existing: existing.year }
1430
+ );
1431
+ }
1432
+ }
1433
+ const fiscalYear = FiscalYear.create(this.ids.next(), year, start, end);
1434
+ this.fiscalYears.add(fiscalYear);
1435
+ return fiscalYear;
1436
+ }
1437
+ createAccount(input) {
1438
+ const actor = this.actor(input);
1439
+ const account = this.buildAccount(input);
1440
+ if (this.accounts.byNumber(account.number) !== null) {
1441
+ throw new DomainError("E_ACCOUNT_NUMBER_TAKEN", `Kontonummer ${account.number.value} ist bereits vergeben`, {
1442
+ number: account.number.value
1443
+ });
1444
+ }
1445
+ this.accounts.add(account);
1446
+ this.recordAudit(actor, "account", account.id, "created");
1447
+ return account;
1448
+ }
1449
+ lockAccount(input) {
1450
+ const actor = this.actor(input);
1451
+ const number = asString(input.number) ?? "";
1452
+ const account = this.accounts.byNumber(AccountNumber.of(number));
1453
+ if (account === null) {
1454
+ throw new DomainError("E_ACCOUNT_UNKNOWN", `Konto ${number} existiert nicht`, { number });
1455
+ }
1456
+ const before = account.status();
1457
+ account.lock();
1458
+ this.accounts.save(account);
1459
+ this.recordAudit(actor, "account", account.id, "locked", {
1460
+ status: { from: before, to: account.status() }
1461
+ });
1462
+ return account;
1463
+ }
1464
+ importChartOfAccounts(input) {
1465
+ const actor = this.actor(input);
1466
+ const rows = input.rows;
1467
+ if (!Array.isArray(rows) || rows.length === 0) {
1468
+ throw new DomainError("E_COA_FORMAT_INVALID", "Import ohne Zeilen");
1469
+ }
1470
+ const accounts = [];
1471
+ const numbers = /* @__PURE__ */ new Set();
1472
+ rows.forEach((row, index) => {
1473
+ if (!isRecord(row)) {
1474
+ throw new DomainError("E_COA_FORMAT_INVALID", `Zeile ${index} ist keine Struktur`);
1475
+ }
1476
+ let account;
1477
+ try {
1478
+ account = this.buildAccount(row);
1479
+ } catch (error) {
1480
+ if (error instanceof DomainError) {
1481
+ throw new DomainError("E_COA_FORMAT_INVALID", `Zeile ${index} ist nicht parsebar`, { row: index });
1482
+ }
1483
+ throw error;
1484
+ }
1485
+ if (numbers.has(account.number.value) || this.accounts.byNumber(account.number) !== null) {
1486
+ throw new DomainError("E_ACCOUNT_NUMBER_TAKEN", `Kontonummer ${account.number.value} ist bereits vergeben`, {
1487
+ number: account.number.value
1488
+ });
1489
+ }
1490
+ numbers.add(account.number.value);
1491
+ accounts.push(account);
1492
+ });
1493
+ for (const account of accounts) {
1494
+ this.accounts.add(account);
1495
+ this.recordAudit(actor, "account", account.id, "created");
1496
+ }
1497
+ return accounts.length;
1498
+ }
1499
+ // ---- intern ----------------------------------------------------------
1500
+ actor(input) {
1501
+ const actor = asString(input.actor);
1502
+ return actor !== null && actor !== "" ? actor : "system";
1503
+ }
1504
+ parseLine(rawLine, index) {
1505
+ const money = rawLine.money;
1506
+ const amount = isRecord(money) ? asString(money.amount) : null;
1507
+ const currency = isRecord(money) ? asString(money.currency) : null;
1508
+ if (amount === null || currency === null) {
1509
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Position ${index}: money fehlt oder unvollst\xE4ndig`);
1510
+ }
1511
+ if (currency !== this.baseCurrency.code) {
1512
+ throw new DomainError(
1513
+ "E_ENTRY_INVALID_AMOUNT",
1514
+ `Position ${index}: Fremdw\xE4hrung ${currency} \u2014 v1 bucht nur Mandantenw\xE4hrung ${this.baseCurrency.code}`,
1515
+ { currency }
1516
+ );
1517
+ }
1518
+ let parsedMoney;
1519
+ try {
1520
+ parsedMoney = Money.of(amount, this.baseCurrency);
1521
+ } catch (error) {
1522
+ if (error instanceof InvalidValue) {
1523
+ throw new DomainError(
1524
+ "E_ENTRY_INVALID_AMOUNT",
1525
+ `Position ${index}: Betrag "${amount}" ist kein g\xFCltiger ${this.baseCurrency.code}-Betrag`,
1526
+ { amount }
1527
+ );
1528
+ }
1529
+ throw error;
1530
+ }
1531
+ if (!parsedMoney.isPositive()) {
1532
+ throw new DomainError(
1533
+ "E_ENTRY_INVALID_AMOUNT",
1534
+ `Position ${index}: Betrag muss > 0 sein (negative Betr\xE4ge nur bei Storno)`,
1535
+ { amount }
1536
+ );
1537
+ }
1538
+ const side = rawLine.side;
1539
+ if (side !== "debit" && side !== "credit") {
1540
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Position ${index}: side muss debit oder credit sein`);
1541
+ }
1542
+ const account = asString(rawLine.account);
1543
+ if (account === null || account === "") {
1544
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Position ${index}: account fehlt`);
1545
+ }
1546
+ const dimensions = [];
1547
+ const rawDimensions = Array.isArray(rawLine.dimensions) ? rawLine.dimensions : [];
1548
+ for (const rawDimension of rawDimensions) {
1549
+ if (!isRecord(rawDimension) || typeof rawDimension.type !== "string" || typeof rawDimension.code !== "string") {
1550
+ throw new DomainError("E_DIMENSION_INVALID", `Position ${index}: Dimension unvollst\xE4ndig`);
1551
+ }
1552
+ dimensions.push(DimensionValue.of(rawDimension.type, rawDimension.code));
1553
+ }
1554
+ const taxTag = isRecord(rawLine.taxTag) ? rawLine.taxTag : null;
1555
+ return { account, side, money: parsedMoney, dimensions, taxTag };
1556
+ }
1557
+ requireVoucher(voucherId) {
1558
+ if (typeof voucherId !== "string" || voucherId === "") {
1559
+ throw new DomainError("E_ENTRY_NO_VOUCHER", "Keine Buchung ohne Beleg (F-CORE-003)");
1560
+ }
1561
+ let voucher = null;
1562
+ try {
1563
+ voucher = this.vouchers.byId(Uuid.fromString(voucherId));
1564
+ } catch (error) {
1565
+ if (!(error instanceof InvalidValue)) throw error;
1566
+ }
1567
+ if (voucher === null) {
1568
+ throw new DomainError("E_VOUCHER_UNKNOWN", `Beleg ${voucherId} existiert nicht`, { voucherId });
1569
+ }
1570
+ return voucher;
1571
+ }
1572
+ resolveLines(parsed) {
1573
+ const lines = parsed.map((line) => {
1574
+ const number = AccountNumber.of(line.account);
1575
+ const account = this.accounts.byNumber(number);
1576
+ if (account === null) {
1577
+ throw new DomainError("E_ACCOUNT_UNKNOWN", `Konto ${number.value} existiert nicht`, { number: number.value });
1578
+ }
1579
+ if (account.isLocked()) {
1580
+ throw new DomainError("E_ACCOUNT_LOCKED", `Konto ${number.value} ist gesperrt`, { number: number.value });
1581
+ }
1582
+ return new EntryLine(account.id, account.number, line.side, line.money, line.dimensions, line.taxTag);
1583
+ });
1584
+ for (const line of lines) {
1585
+ this.dimensions.validateLine(line.account, line.dimensions);
1586
+ }
1587
+ return lines;
1588
+ }
1589
+ assertBalanced(lines) {
1590
+ let debit = Money.zero(this.baseCurrency);
1591
+ let credit = Money.zero(this.baseCurrency);
1592
+ for (const line of lines) {
1593
+ if (line.side === "debit") debit = debit.add(line.money);
1594
+ else credit = credit.add(line.money);
1595
+ }
1596
+ if (!debit.equals(credit)) {
1597
+ throw new DomainError(
1598
+ "E_ENTRY_UNBALANCED",
1599
+ `\u03A3 Soll (${debit.amountAsString()}) \u2260 \u03A3 Haben (${credit.amountAsString()})`,
1600
+ { debit: debit.amountAsString(), credit: credit.amountAsString() }
1601
+ );
1602
+ }
1603
+ }
1604
+ parseEntryDate(entryDate) {
1605
+ if (typeof entryDate !== "string") {
1606
+ throw new DomainError("E_PERIOD_UNKNOWN", "entryDate fehlt");
1607
+ }
1608
+ try {
1609
+ return CalendarDate.of(entryDate);
1610
+ } catch (error) {
1611
+ if (error instanceof InvalidValue) {
1612
+ throw new DomainError("E_PERIOD_UNKNOWN", `Ung\xFCltiges Buchungsdatum "${entryDate}"`);
1613
+ }
1614
+ throw error;
1615
+ }
1616
+ }
1617
+ openPeriodFor(entryDate) {
1618
+ const fiscalYear = this.fiscalYears.forDate(entryDate);
1619
+ if (fiscalYear === null) {
1620
+ throw new DomainError(
1621
+ "E_PERIOD_UNKNOWN",
1622
+ `Buchungsdatum ${entryDate.iso} liegt au\xDFerhalb angelegter Gesch\xE4ftsjahre`,
1623
+ { date: entryDate.iso }
1624
+ );
1625
+ }
1626
+ const period = fiscalYear.periodForDate(entryDate);
1627
+ if (fiscalYear.isClosed() || !period.isOpen()) {
1628
+ throw new DomainError("E_PERIOD_CLOSED", `Periode ${fiscalYear.year}/${period.number} ist geschlossen`, {
1629
+ fiscalYear: fiscalYear.year,
1630
+ period: period.number
1631
+ });
1632
+ }
1633
+ return [fiscalYear, period];
1634
+ }
1635
+ requireEntry(entryId) {
1636
+ let entry = null;
1637
+ if (typeof entryId === "string" && entryId !== "") {
1638
+ try {
1639
+ entry = this.journal.byId(Uuid.fromString(entryId));
1640
+ } catch (error) {
1641
+ if (!(error instanceof InvalidValue)) throw error;
1642
+ }
1643
+ }
1644
+ if (entry === null) {
1645
+ throw new DomainError("E_ENTRY_UNKNOWN", `Buchung ${typeof entryId === "string" ? entryId : "?"} existiert nicht`);
1646
+ }
1647
+ return entry;
1648
+ }
1649
+ requireFiscalYear(year) {
1650
+ const fiscalYear = typeof year === "number" ? this.fiscalYears.byYear(year) : null;
1651
+ if (fiscalYear === null) {
1652
+ throw new DomainError("E_PERIOD_UNKNOWN", `Gesch\xE4ftsjahr ${typeof year === "number" ? year : "?"} ist nicht angelegt`);
1653
+ }
1654
+ return fiscalYear;
1655
+ }
1656
+ periodNumber(input) {
1657
+ const period = input.period;
1658
+ if (typeof period !== "number" || !Number.isInteger(period)) {
1659
+ throw new DomainError("E_PERIOD_UNKNOWN", "Periodennummer fehlt");
1660
+ }
1661
+ return period;
1662
+ }
1663
+ buildAccount(input) {
1664
+ const number = asString(input.number);
1665
+ const name = asString(input.name);
1666
+ const type = input.type;
1667
+ if (number === null || number === "" || name === null || name === "" || !isAccountType(type)) {
1668
+ throw new DomainError("E_COA_FORMAT_INVALID", "Konto braucht number, name und g\xFCltigen type");
1669
+ }
1670
+ const subtype = asString(input.subtype);
1671
+ const status = input.status === "locked" ? "locked" : "active";
1672
+ return new Account(this.ids.next(), AccountNumber.of(number), name, type, subtype, status);
1673
+ }
1674
+ recordAudit(actor, objectType, objectId, action, changes = {}) {
1675
+ this.audit.append(new AuditRecord(this.ids.next(), this.now(), actor, objectType, objectId, action, changes));
1676
+ }
1677
+ now() {
1678
+ return this.clock.now().toISOString();
1679
+ }
1680
+ };
1681
+
1682
+ // src/in-memory.ts
1683
+ var InMemoryAccountRepository = class {
1684
+ byNumberMap = /* @__PURE__ */ new Map();
1685
+ byIdMap = /* @__PURE__ */ new Map();
1686
+ add(account) {
1687
+ if (this.byNumberMap.has(account.number.value)) {
1688
+ throw new Error(`Repository-Kontrakt verletzt: Kontonummer ${account.number.value} doppelt`);
1689
+ }
1690
+ this.byNumberMap.set(account.number.value, account);
1691
+ this.byIdMap.set(account.id.value, account);
1692
+ }
1693
+ save(_account) {
1694
+ }
1695
+ byNumber(number) {
1696
+ return this.byNumberMap.get(number.value) ?? null;
1697
+ }
1698
+ byId(id) {
1699
+ return this.byIdMap.get(id.value) ?? null;
1700
+ }
1701
+ all() {
1702
+ return [...this.byNumberMap.values()].sort((a, b) => a.number.compareTo(b.number));
1703
+ }
1704
+ };
1705
+ var InMemoryFiscalYearRepository = class {
1706
+ byYearMap = /* @__PURE__ */ new Map();
1707
+ add(fiscalYear) {
1708
+ this.byYearMap.set(fiscalYear.year, fiscalYear);
1709
+ }
1710
+ save(_fiscalYear) {
1711
+ }
1712
+ byYear(year) {
1713
+ return this.byYearMap.get(year) ?? null;
1714
+ }
1715
+ forDate(date) {
1716
+ for (const fiscalYear of this.byYearMap.values()) {
1717
+ if (fiscalYear.contains(date)) return fiscalYear;
1718
+ }
1719
+ return null;
1720
+ }
1721
+ all() {
1722
+ return [...this.byYearMap.values()].sort((a, b) => a.year - b.year);
1723
+ }
1724
+ };
1725
+ var InMemoryJournalRepository = class {
1726
+ entries = [];
1727
+ byIdMap = /* @__PURE__ */ new Map();
1728
+ sequences = /* @__PURE__ */ new Map();
1729
+ append(entry) {
1730
+ this.entries.push(entry);
1731
+ this.byIdMap.set(entry.id.value, entry);
1732
+ this.sequences.set(entry.periodRef.fiscalYear, entry.sequenceNumber);
1733
+ }
1734
+ save(_entry) {
1735
+ }
1736
+ byId(id) {
1737
+ return this.byIdMap.get(id.value) ?? null;
1738
+ }
1739
+ nextSequenceNumber(fiscalYear) {
1740
+ return (this.sequences.get(fiscalYear) ?? 0) + 1;
1741
+ }
1742
+ all() {
1743
+ return [...this.entries].sort(
1744
+ (a, b) => a.periodRef.fiscalYear !== b.periodRef.fiscalYear ? a.periodRef.fiscalYear - b.periodRef.fiscalYear : a.sequenceNumber - b.sequenceNumber
1745
+ );
1746
+ }
1747
+ forFiscalYear(fiscalYear) {
1748
+ return this.entries.filter((entry) => entry.periodRef.fiscalYear === fiscalYear).sort((a, b) => a.sequenceNumber - b.sequenceNumber);
1749
+ }
1750
+ };
1751
+ var InMemoryVoucherRepository = class {
1752
+ byIdMap = /* @__PURE__ */ new Map();
1753
+ add(voucher) {
1754
+ this.byIdMap.set(voucher.id.value, voucher);
1755
+ }
1756
+ byId(id) {
1757
+ return this.byIdMap.get(id.value) ?? null;
1758
+ }
1759
+ all() {
1760
+ return [...this.byIdMap.values()].sort(
1761
+ (a, b) => a.id.value < b.id.value ? -1 : a.id.value > b.id.value ? 1 : 0
1762
+ );
1763
+ }
1764
+ };
1765
+ var InMemoryOpenItemRepository = class {
1766
+ items = [];
1767
+ byIdMap = /* @__PURE__ */ new Map();
1768
+ add(item) {
1769
+ this.items.push(item);
1770
+ this.byIdMap.set(item.id.value, item);
1771
+ }
1772
+ save(_item) {
1773
+ }
1774
+ byId(id) {
1775
+ return this.byIdMap.get(id.value) ?? null;
1776
+ }
1777
+ byOriginEntry(entryId) {
1778
+ return this.items.filter((item) => item.originEntryId.equals(entryId));
1779
+ }
1780
+ all() {
1781
+ return [...this.items];
1782
+ }
1783
+ };
1784
+ var InMemoryAuditTrail = class {
1785
+ records = [];
1786
+ append(record) {
1787
+ this.records.push(record);
1788
+ }
1789
+ all() {
1790
+ return [...this.records];
1791
+ }
1792
+ };
1793
+ var InMemoryPartnerRepository = class {
1794
+ byIdMap = /* @__PURE__ */ new Map();
1795
+ add(partner) {
1796
+ this.byIdMap.set(partner.id.value, partner);
1797
+ }
1798
+ save(_partner) {
1799
+ }
1800
+ byId(id) {
1801
+ return this.byIdMap.get(id.value) ?? null;
1802
+ }
1803
+ all() {
1804
+ return [...this.byIdMap.values()].sort((a, b) => {
1805
+ const byName = a.name() < b.name() ? -1 : a.name() > b.name() ? 1 : 0;
1806
+ return byName !== 0 ? byName : a.id.value < b.id.value ? -1 : a.id.value > b.id.value ? 1 : 0;
1807
+ });
1808
+ }
1809
+ };
1810
+ var InMemoryAssetRepository = class {
1811
+ items = [];
1812
+ byIdMap = /* @__PURE__ */ new Map();
1813
+ add(asset) {
1814
+ this.items.push(asset);
1815
+ this.byIdMap.set(asset.id.value, asset);
1816
+ }
1817
+ save(_asset) {
1818
+ }
1819
+ byId(id) {
1820
+ return this.byIdMap.get(id.value) ?? null;
1821
+ }
1822
+ all() {
1823
+ return [...this.items];
1824
+ }
1825
+ };
1826
+
1827
+ // src/assets/asset.ts
1828
+ function lastDayOfMonthAfter(base, monthsToAdd) {
1829
+ const totalMonth0 = base.month() - 1 + monthsToAdd;
1830
+ const year = base.year() + Math.floor(totalMonth0 / 12);
1831
+ const month0 = (totalMonth0 % 12 + 12) % 12;
1832
+ const lastDay = new Date(Date.UTC(year, month0 + 1, 0)).getUTCDate();
1833
+ return CalendarDate.of(
1834
+ `${String(year).padStart(4, "0")}-${String(month0 + 1).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`
1835
+ );
1836
+ }
1837
+ var Asset = class {
1838
+ constructor(id, name, assetClass, assetAccount, acquisitionCost, acquiredOn, route, usefulLifeMonths, monthlySchedule, voucherId) {
1839
+ this.id = id;
1840
+ this.name = name;
1841
+ this.assetClass = assetClass;
1842
+ this.assetAccount = assetAccount;
1843
+ this.acquisitionCost = acquisitionCost;
1844
+ this.acquiredOn = acquiredOn;
1845
+ this.route = route;
1846
+ this.usefulLifeMonths = usefulLifeMonths;
1847
+ this.monthlySchedule = monthlySchedule;
1848
+ this.voucherId = voucherId;
1849
+ }
1850
+ id;
1851
+ name;
1852
+ assetClass;
1853
+ assetAccount;
1854
+ acquisitionCost;
1855
+ acquiredOn;
1856
+ route;
1857
+ usefulLifeMonths;
1858
+ monthlySchedule;
1859
+ voucherId;
1860
+ depreciations = [];
1861
+ disposed = false;
1862
+ disposedOn = null;
1863
+ isDisposed() {
1864
+ return this.disposed;
1865
+ }
1866
+ assertActive() {
1867
+ if (this.disposed) {
1868
+ throw new DomainError(
1869
+ "E_ASSET_DISPOSED",
1870
+ `Anlagegut ${this.id.value} ist bereits abgegangen (${this.disposedOn?.iso ?? "?"})`,
1871
+ { assetId: this.id.value }
1872
+ );
1873
+ }
1874
+ }
1875
+ dispose(disposedOn) {
1876
+ this.assertActive();
1877
+ this.disposed = true;
1878
+ this.disposedOn = disposedOn;
1879
+ }
1880
+ planMonthDate(planMonth) {
1881
+ return lastDayOfMonthAfter(this.acquiredOn, planMonth - 1);
1882
+ }
1883
+ isMonthBooked(planMonth) {
1884
+ return this.depreciations.some((booking) => booking.planMonth === planMonth);
1885
+ }
1886
+ recordDepreciation(planMonth, date, amount, entryId) {
1887
+ this.depreciations.push({ planMonth, date, amount, entryId });
1888
+ }
1889
+ accumulatedDepreciationAt(asOf) {
1890
+ let sum = this.acquisitionCost.subtract(this.acquisitionCost);
1891
+ for (const booking of this.depreciations) {
1892
+ if (asOf !== null && booking.date.isAfter(asOf)) continue;
1893
+ sum = sum.add(booking.amount);
1894
+ }
1895
+ return sum;
1896
+ }
1897
+ bookValueAt(asOf) {
1898
+ if (this.route !== "capitalize") return this.acquisitionCost.subtract(this.acquisitionCost);
1899
+ return this.acquisitionCost.subtract(this.accumulatedDepreciationAt(asOf));
1900
+ }
1901
+ scheduleSummary() {
1902
+ if (this.monthlySchedule.length === 0) return {};
1903
+ const summary = {};
1904
+ let total = this.acquisitionCost.subtract(this.acquisitionCost);
1905
+ let runStart = 1;
1906
+ this.monthlySchedule.forEach((amount, index) => {
1907
+ total = total.add(amount);
1908
+ const isLast = index === this.monthlySchedule.length - 1;
1909
+ const next = isLast ? null : this.monthlySchedule[index + 1];
1910
+ if (next !== null && next.equals(amount)) return;
1911
+ summary[`months${runStart}to${index + 1}`] = amount.amountAsString();
1912
+ runStart = index + 2;
1913
+ });
1914
+ summary.total = total.amountAsString();
1915
+ return summary;
1916
+ }
1917
+ toJSON() {
1918
+ return {
1919
+ id: this.id.value,
1920
+ name: this.name,
1921
+ assetClass: this.assetClass,
1922
+ assetAccount: this.assetAccount.value,
1923
+ route: this.route,
1924
+ acquisitionCost: this.acquisitionCost.toJSON(),
1925
+ acquiredOn: this.acquiredOn.iso,
1926
+ usefulLifeMonths: this.usefulLifeMonths,
1927
+ status: this.disposed ? "disposed" : "active",
1928
+ disposedOn: this.disposedOn?.iso ?? null,
1929
+ voucherId: this.voucherId.value
1930
+ };
1931
+ }
1932
+ };
1933
+
1934
+ // src/assets/asset-route.ts
1935
+ function parseAssetRoute(value) {
1936
+ return value === "capitalize" || value === "immediate_expense" || value === "pool" ? value : null;
1937
+ }
1938
+
1939
+ // src/assets/asset-service.ts
1940
+ function isRecord2(value) {
1941
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1942
+ }
1943
+ function asString2(value) {
1944
+ return typeof value === "string" ? value : null;
1945
+ }
1946
+ var AssetService = class {
1947
+ constructor(baseCurrency, assets, fiscalYears, vouchers, ledger, ids) {
1948
+ this.baseCurrency = baseCurrency;
1949
+ this.assets = assets;
1950
+ this.fiscalYears = fiscalYears;
1951
+ this.vouchers = vouchers;
1952
+ this.ledger = ledger;
1953
+ this.ids = ids;
1954
+ }
1955
+ baseCurrency;
1956
+ assets;
1957
+ fiscalYears;
1958
+ vouchers;
1959
+ ledger;
1960
+ ids;
1961
+ ruleModule = {};
1962
+ setRuleModule(ruleModule) {
1963
+ this.ruleModule = ruleModule;
1964
+ }
1965
+ acquire(input) {
1966
+ const name = asString2(input.name) ?? "";
1967
+ const assetClass = asString2(input.assetClass) ?? "";
1968
+ const assetAccount = AccountNumber.of(asString2(input.assetAccount) ?? "0");
1969
+ const cost = this.parseMoney(input.acquisitionCost);
1970
+ const acquiredOn = CalendarDate.of(asString2(input.acquiredOn) ?? "");
1971
+ const voucherIdRaw = asString2(input.voucherId);
1972
+ if (voucherIdRaw === null) throw new InvalidValue("acquireAsset braucht voucherId");
1973
+ const voucherId = Uuid.fromString(voucherIdRaw);
1974
+ const choice = asString2(input.gwgChoice) ?? "auto";
1975
+ const route = this.resolveRoute(choice, cost, acquiredOn);
1976
+ let usefulLifeMonths = null;
1977
+ const schedule = [];
1978
+ if (route === "capitalize") {
1979
+ usefulLifeMonths = this.usefulLifeMonths(assetClass);
1980
+ schedule.push(...cost.allocateEvenly(usefulLifeMonths));
1981
+ } else if (route === "pool") {
1982
+ usefulLifeMonths = 60;
1983
+ for (const yearAmount of cost.allocateEvenly(5)) {
1984
+ schedule.push(...yearAmount.allocateEvenly(12));
1985
+ }
1986
+ }
1987
+ const asset = new Asset(
1988
+ this.ids.next(),
1989
+ name,
1990
+ assetClass,
1991
+ assetAccount,
1992
+ cost,
1993
+ acquiredOn,
1994
+ route,
1995
+ usefulLifeMonths,
1996
+ schedule,
1997
+ voucherId
1998
+ );
1999
+ this.assets.add(asset);
2000
+ const targetAccount = route === "immediate_expense" ? this.gwgExpenseAccount() : assetAccount.value;
2001
+ this.postMachineEntry(acquiredOn, voucherId, `Anlagenzugang ${name}`, [
2002
+ { account: targetAccount, side: "debit", money: cost.toJSON() },
2003
+ { account: this.counterAccount(), side: "credit", money: cost.toJSON() }
2004
+ ]);
2005
+ const result = asset.toJSON();
2006
+ result.route = route;
2007
+ if (route === "immediate_expense") result.expenseAccount = targetAccount;
2008
+ return result;
2009
+ }
2010
+ dispose(input) {
2011
+ const asset = this.requireAsset(input.assetId);
2012
+ asset.assertActive();
2013
+ const disposedOn = CalendarDate.of(asString2(input.disposedOn) ?? "");
2014
+ asset.dispose(disposedOn);
2015
+ this.assets.save(asset);
2016
+ const proceeds = isRecord2(input.proceeds) ? this.parseMoney(input.proceeds) : null;
2017
+ const proceedsAccount = asString2(input.proceedsAccount);
2018
+ const bankAccount = asString2(input.bankAccount) ?? this.counterAccount();
2019
+ if (proceeds !== null && proceedsAccount !== null) {
2020
+ const voucherId = asString2(input.voucherId) ? Uuid.fromString(asString2(input.voucherId)) : asset.voucherId;
2021
+ this.postMachineEntry(disposedOn, voucherId, `Anlagenabgang ${asset.name}`, [
2022
+ { account: bankAccount, side: "debit", money: proceeds.toJSON() },
2023
+ { account: proceedsAccount, side: "credit", money: proceeds.toJSON() }
2024
+ ]);
2025
+ }
2026
+ return asset.toJSON();
2027
+ }
2028
+ runDepreciation(input) {
2029
+ const fiscalYear = typeof input.fiscalYear === "number" ? input.fiscalYear : 0;
2030
+ const period = typeof input.period === "number" ? input.period : null;
2031
+ let entriesCreated = 0;
2032
+ let total = Money.zero(this.baseCurrency);
2033
+ for (const asset of this.assets.all()) {
2034
+ if (asset.route !== "capitalize" && asset.route !== "pool") continue;
2035
+ if (asset.isDisposed()) continue;
2036
+ const [months, amount] = period === null ? this.yearTarget(asset, fiscalYear) : this.monthTarget(asset, fiscalYear, period);
2037
+ if (months.length === 0 || amount.isZero()) continue;
2038
+ const bookingDate = this.bookingDate(asset, fiscalYear, period, months);
2039
+ const periodLabel = period === null ? "" : `/${String(period).padStart(2, "0")}`;
2040
+ const entry = this.postMachineEntry(
2041
+ bookingDate,
2042
+ this.depreciationVoucher(asset, fiscalYear, period),
2043
+ `AfA ${asset.name} ${fiscalYear}${periodLabel}`,
2044
+ [
2045
+ { account: this.depreciationExpenseAccount(), side: "debit", money: amount.toJSON() },
2046
+ { account: asset.assetAccount.value, side: "credit", money: amount.toJSON() }
2047
+ ]
2048
+ );
2049
+ const monthAmounts = months.length === 1 ? [amount] : this.monthAmounts(asset, months, amount);
2050
+ months.forEach((planMonth, index) => {
2051
+ asset.recordDepreciation(planMonth, bookingDate, monthAmounts[index], entry);
2052
+ });
2053
+ this.assets.save(asset);
2054
+ entriesCreated++;
2055
+ total = total.add(amount);
2056
+ }
2057
+ if (entriesCreated === 0) return { alreadyRun: true, entriesCreated: 0 };
2058
+ return { entriesCreated, totalDepreciation: total.toJSON() };
2059
+ }
2060
+ requireAsset(assetId) {
2061
+ let asset = null;
2062
+ if (typeof assetId === "string" && assetId !== "") {
2063
+ try {
2064
+ asset = this.assets.byId(Uuid.fromString(assetId));
2065
+ } catch (error) {
2066
+ if (!(error instanceof InvalidValue)) throw error;
2067
+ }
2068
+ }
2069
+ if (asset === null) {
2070
+ throw new DomainError("E_ASSET_UNKNOWN", `Anlagegut ${typeof assetId === "string" ? assetId : "?"} existiert nicht`);
2071
+ }
2072
+ return asset;
2073
+ }
2074
+ // ---- intern ----------------------------------------------------------
2075
+ yearTarget(asset, fiscalYear) {
2076
+ const zero = Money.zero(this.baseCurrency);
2077
+ const monthsByYear = /* @__PURE__ */ new Map();
2078
+ const life = asset.monthlySchedule.length;
2079
+ for (let planMonth = 1; planMonth <= life; planMonth++) {
2080
+ const year = asset.planMonthDate(planMonth).year();
2081
+ const list = monthsByYear.get(year) ?? [];
2082
+ list.push(planMonth);
2083
+ monthsByYear.set(year, list);
2084
+ }
2085
+ const months = monthsByYear.get(fiscalYear);
2086
+ if (months === void 0) return [[], zero];
2087
+ const years = [...monthsByYear.keys()];
2088
+ const weights = years.map((year) => monthsByYear.get(year).length);
2089
+ const yearAmounts = asset.acquisitionCost.allocate(...weights);
2090
+ const yearIndex = years.indexOf(fiscalYear);
2091
+ if (yearIndex === -1) return [[], zero];
2092
+ const yearAmount = yearAmounts[yearIndex];
2093
+ const openMonths = [];
2094
+ let bookedAmount = zero;
2095
+ for (const planMonth of months) {
2096
+ if (asset.isMonthBooked(planMonth)) {
2097
+ bookedAmount = bookedAmount.add(asset.monthlySchedule[planMonth - 1]);
2098
+ continue;
2099
+ }
2100
+ openMonths.push(planMonth);
2101
+ }
2102
+ const amount = yearAmount.subtract(bookedAmount);
2103
+ if (openMonths.length === 0 || !amount.isPositive()) return [[], zero];
2104
+ return [openMonths, amount];
2105
+ }
2106
+ monthTarget(asset, fiscalYear, period) {
2107
+ const zero = Money.zero(this.baseCurrency);
2108
+ const year = this.fiscalYears.byYear(fiscalYear);
2109
+ if (year === null) {
2110
+ throw new DomainError("E_PERIOD_UNKNOWN", `Gesch\xE4ftsjahr ${fiscalYear} ist nicht angelegt`);
2111
+ }
2112
+ const periodEntity = year.period(period);
2113
+ const life = asset.monthlySchedule.length;
2114
+ for (let planMonth = 1; planMonth <= life; planMonth++) {
2115
+ const date = asset.planMonthDate(planMonth);
2116
+ if (!periodEntity.contains(date)) continue;
2117
+ if (asset.isMonthBooked(planMonth)) return [[], zero];
2118
+ return [[planMonth], asset.monthlySchedule[planMonth - 1]];
2119
+ }
2120
+ return [[], zero];
2121
+ }
2122
+ monthAmounts(asset, months, total) {
2123
+ const planned = months.map((planMonth) => asset.monthlySchedule[planMonth - 1]);
2124
+ let plannedSum = Money.zero(this.baseCurrency);
2125
+ for (const amount of planned) plannedSum = plannedSum.add(amount);
2126
+ if (plannedSum.equals(total)) return planned;
2127
+ return total.allocateEvenly(months.length);
2128
+ }
2129
+ bookingDate(asset, fiscalYear, period, months) {
2130
+ const year = this.fiscalYears.byYear(fiscalYear);
2131
+ if (period !== null && year !== null) return year.period(period).end;
2132
+ if (year !== null) return year.end;
2133
+ return asset.planMonthDate(months[months.length - 1]);
2134
+ }
2135
+ postMachineEntry(date, voucherId, text, lines) {
2136
+ const result = this.ledger.post({ entryDate: date.iso, voucherId: voucherId.value, text, lines });
2137
+ this.ledger.finalize({ entryId: result.entry.id.value });
2138
+ return result.entry.id;
2139
+ }
2140
+ depreciationVoucher(asset, fiscalYear, period) {
2141
+ const periodLabel = period === null ? "" : `-${String(period).padStart(2, "0")}`;
2142
+ const voucher = new Voucher({
2143
+ id: this.ids.next(),
2144
+ voucherNumber: `AFA-${fiscalYear}${periodLabel}-${asset.id.value.slice(-6)}`,
2145
+ voucherDate: CalendarDate.of(`${String(fiscalYear).padStart(4, "0")}-12-31`),
2146
+ kind: "internal"
2147
+ });
2148
+ this.vouchers.add(voucher);
2149
+ return voucher.id;
2150
+ }
2151
+ resolveRoute(choice, cost, acquiredOn) {
2152
+ if (choice !== "auto") return parseAssetRoute(choice) ?? "capitalize";
2153
+ for (const threshold of this.thresholds()) {
2154
+ const validFrom = CalendarDate.of(threshold.validFrom);
2155
+ const validTo = threshold.validTo === null ? null : CalendarDate.of(threshold.validTo);
2156
+ if (acquiredOn.isBefore(validFrom) || validTo !== null && acquiredOn.isAfter(validTo)) continue;
2157
+ if (cost.compareTo(Money.of(threshold.immediateMax, this.baseCurrency)) <= 0) return "immediate_expense";
2158
+ if (threshold.poolMin !== null && threshold.poolMax !== null && cost.compareTo(Money.of(threshold.poolMin, this.baseCurrency)) >= 0 && cost.compareTo(Money.of(threshold.poolMax, this.baseCurrency)) <= 0) {
2159
+ return "pool";
2160
+ }
2161
+ }
2162
+ return "capitalize";
2163
+ }
2164
+ thresholds() {
2165
+ const raw = Array.isArray(this.ruleModule.gwgThresholds) ? this.ruleModule.gwgThresholds : [];
2166
+ const thresholds = [];
2167
+ for (const item of raw) {
2168
+ if (!isRecord2(item) || typeof item.validFrom !== "string" || typeof item.immediateMax !== "string") continue;
2169
+ thresholds.push({
2170
+ validFrom: item.validFrom,
2171
+ validTo: typeof item.validTo === "string" ? item.validTo : null,
2172
+ immediateMax: item.immediateMax,
2173
+ poolMin: typeof item.poolMin === "string" ? item.poolMin : null,
2174
+ poolMax: typeof item.poolMax === "string" ? item.poolMax : null
2175
+ });
2176
+ }
2177
+ return thresholds;
2178
+ }
2179
+ usefulLifeMonths(assetClass) {
2180
+ const raw = Array.isArray(this.ruleModule.usefulLife) ? this.ruleModule.usefulLife : [];
2181
+ for (const item of raw) {
2182
+ if (isRecord2(item) && item.assetClass === assetClass && typeof item.months === "number") {
2183
+ return item.months;
2184
+ }
2185
+ }
2186
+ throw new DomainError(
2187
+ "E_ASSET_UNKNOWN",
2188
+ `Keine Nutzungsdauer f\xFCr Anlagenklasse "${assetClass}" im Regelmodul (siehe SPEC-FINDINGS)`
2189
+ );
2190
+ }
2191
+ counterAccount() {
2192
+ return this.assetAccount("acquisitionCounterAccount");
2193
+ }
2194
+ depreciationExpenseAccount() {
2195
+ return this.assetAccount("depreciationExpenseAccount");
2196
+ }
2197
+ gwgExpenseAccount() {
2198
+ return this.assetAccount("gwgExpenseAccount");
2199
+ }
2200
+ assetAccount(key) {
2201
+ const block = isRecord2(this.ruleModule.assetAccounts) ? this.ruleModule.assetAccounts : {};
2202
+ const value = block[key];
2203
+ if (typeof value === "string" && value !== "") return value;
2204
+ throw new DomainError("E_ACCOUNT_UNKNOWN", `assetAccounts.${key} ist im Regelmodul nicht gesetzt`, { key });
2205
+ }
2206
+ parseMoney(raw) {
2207
+ const amount = isRecord2(raw) ? asString2(raw.amount) : null;
2208
+ if (amount === null) throw new InvalidValue("Betrag fehlt");
2209
+ return Money.of(amount, this.baseCurrency);
2210
+ }
2211
+ };
2212
+
2213
+ // src/costing/costing-run.ts
2214
+ var CostingRun = class {
2215
+ constructor(id, period, version, primary, afterAllocation, grandTotal) {
2216
+ this.id = id;
2217
+ this.period = period;
2218
+ this.version = version;
2219
+ this.primary = primary;
2220
+ this.afterAllocation = afterAllocation;
2221
+ this.grandTotal = grandTotal;
2222
+ }
2223
+ id;
2224
+ period;
2225
+ version;
2226
+ primary;
2227
+ afterAllocation;
2228
+ grandTotal;
2229
+ runStatus = "draft";
2230
+ status() {
2231
+ return this.runStatus;
2232
+ }
2233
+ release() {
2234
+ if (this.runStatus === "released") {
2235
+ throw new DomainError(
2236
+ "E_COSTING_RUN_RELEASED",
2237
+ `Lauf ${this.id.value} ist bereits freigegeben \u2014 \xC4nderungen erzeugen eine neue Version`,
2238
+ { runId: this.id.value }
2239
+ );
2240
+ }
2241
+ this.runStatus = "released";
2242
+ }
2243
+ };
2244
+
2245
+ // src/costing/costing-service.ts
2246
+ function isRecord3(value) {
2247
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2248
+ }
2249
+ var CostingService = class {
2250
+ constructor(baseCurrency, accounts, journal, ids) {
2251
+ this.baseCurrency = baseCurrency;
2252
+ this.accounts = accounts;
2253
+ this.journal = journal;
2254
+ this.ids = ids;
2255
+ }
2256
+ baseCurrency;
2257
+ accounts;
2258
+ journal;
2259
+ ids;
2260
+ schemeSteps = [];
2261
+ runs = /* @__PURE__ */ new Map();
2262
+ versions = /* @__PURE__ */ new Map();
2263
+ setAllocationScheme(input) {
2264
+ const method = typeof input.method === "string" ? input.method : "step_ladder";
2265
+ const steps = [];
2266
+ const edges = /* @__PURE__ */ new Map();
2267
+ for (const rawStep of Array.isArray(input.steps) ? input.steps : []) {
2268
+ if (!isRecord3(rawStep) || typeof rawStep.sender !== "string") {
2269
+ throw new InvalidValue("Umlageschritt braucht sender");
2270
+ }
2271
+ const sender = rawStep.sender;
2272
+ const receivers = [];
2273
+ for (const rawReceiver of Array.isArray(rawStep.receivers) ? rawStep.receivers : []) {
2274
+ if (!isRecord3(rawReceiver) || typeof rawReceiver.code !== "string") continue;
2275
+ receivers.push({ code: rawReceiver.code, share: typeof rawReceiver.share === "string" ? rawReceiver.share : "1" });
2276
+ const list = edges.get(sender) ?? [];
2277
+ list.push(rawReceiver.code);
2278
+ edges.set(sender, list);
2279
+ }
2280
+ steps.push({ sender, receivers });
2281
+ }
2282
+ if (method === "step_ladder") this.assertAcyclic(edges);
2283
+ this.schemeSteps = steps;
2284
+ return { valid: true, method, stepCount: steps.length };
2285
+ }
2286
+ run(input) {
2287
+ const fiscalYear = typeof input.fiscalYear === "number" ? input.fiscalYear : 0;
2288
+ const period = typeof input.period === "number" ? input.period : 0;
2289
+ const periodRef = new PeriodRef(fiscalYear, period);
2290
+ const zero = Money.zero(this.baseCurrency);
2291
+ const primary = /* @__PURE__ */ new Map();
2292
+ for (const entry of this.journal.forFiscalYear(fiscalYear)) {
2293
+ if (entry.periodRef.period !== period) continue;
2294
+ for (const line of entry.lines()) {
2295
+ const account = this.accounts.byId(line.accountId);
2296
+ if (account === null || account.type !== "expense") continue;
2297
+ for (const dimension of line.dimensions) {
2298
+ if (dimension.type !== "costCenter") continue;
2299
+ const signed = line.side === "debit" ? line.money : line.money.negate();
2300
+ primary.set(dimension.code, (primary.get(dimension.code) ?? zero).add(signed));
2301
+ }
2302
+ }
2303
+ }
2304
+ const after = new Map(primary);
2305
+ for (const step of this.schemeSteps) {
2306
+ const senderTotal = after.get(step.sender) ?? zero;
2307
+ if (senderTotal.isZero() || step.receivers.length === 0) continue;
2308
+ const weights = step.receivers.map((receiver) => receiver.share);
2309
+ const parts = senderTotal.allocate(...weights);
2310
+ step.receivers.forEach((receiver, index) => {
2311
+ after.set(receiver.code, (after.get(receiver.code) ?? zero).add(parts[index]));
2312
+ });
2313
+ after.set(step.sender, zero);
2314
+ }
2315
+ let grandTotal = zero;
2316
+ for (const total of after.values()) grandTotal = grandTotal.add(total);
2317
+ const key = `${fiscalYear}-${period}`;
2318
+ const version = (this.versions.get(key) ?? 0) + 1;
2319
+ this.versions.set(key, version);
2320
+ const run = new CostingRun(this.ids.next(), periodRef, version, primary, after, grandTotal);
2321
+ this.runs.set(run.id.value, run);
2322
+ return run;
2323
+ }
2324
+ release(input) {
2325
+ const run = this.requireRun(input.runId);
2326
+ run.release();
2327
+ return run;
2328
+ }
2329
+ costAllocationSheet(params) {
2330
+ const run = this.requireRun(params.runId);
2331
+ return {
2332
+ runId: run.id.value,
2333
+ status: run.status(),
2334
+ version: run.version,
2335
+ primary: this.serializeTotals(run.primary),
2336
+ afterAllocation: this.serializeTotals(run.afterAllocation),
2337
+ grandTotal: run.grandTotal.amountAsString()
2338
+ };
2339
+ }
2340
+ serializeTotals(totals) {
2341
+ const codes = [...totals.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2342
+ return codes.map((code) => ({ costCenter: code, total: totals.get(code).amountAsString() }));
2343
+ }
2344
+ requireRun(runId) {
2345
+ let run = null;
2346
+ if (typeof runId === "string" && runId !== "") {
2347
+ try {
2348
+ run = this.runs.get(Uuid.fromString(runId).value) ?? null;
2349
+ } catch (error) {
2350
+ if (!(error instanceof InvalidValue)) throw error;
2351
+ }
2352
+ }
2353
+ if (run === null) {
2354
+ throw new DomainError("E_COSTING_RUN_UNKNOWN", `Abrechnungslauf ${typeof runId === "string" ? runId : "?"} existiert nicht`);
2355
+ }
2356
+ return run;
2357
+ }
2358
+ assertAcyclic(edges) {
2359
+ const visiting = /* @__PURE__ */ new Set();
2360
+ const done = /* @__PURE__ */ new Set();
2361
+ const visit = (node) => {
2362
+ if (done.has(node)) return;
2363
+ if (visiting.has(node)) {
2364
+ throw new DomainError(
2365
+ "E_COSTING_CYCLE",
2366
+ `Umlagezyklus \xFCber Kostenstelle "${node}" \u2014 Stufenleiter verlangt Zyklenfreiheit`,
2367
+ { costCenter: node }
2368
+ );
2369
+ }
2370
+ visiting.add(node);
2371
+ for (const next of edges.get(node) ?? []) visit(next);
2372
+ visiting.delete(node);
2373
+ done.add(node);
2374
+ };
2375
+ for (const node of edges.keys()) visit(node);
2376
+ }
2377
+ };
2378
+
2379
+ // src/partner/partner.ts
2380
+ var Partner = class {
2381
+ constructor(id, name, kind, vatId, paymentTermsDays, accountNumbers = [], address = {}) {
2382
+ this.id = id;
2383
+ this.accountNumbers = accountNumbers;
2384
+ this.address = address;
2385
+ this.partnerName = name;
2386
+ this.partnerKind = kind;
2387
+ this.partnerVatId = vatId;
2388
+ this.partnerPaymentTermsDays = paymentTermsDays;
2389
+ }
2390
+ id;
2391
+ accountNumbers;
2392
+ address;
2393
+ partnerName;
2394
+ partnerKind;
2395
+ partnerVatId;
2396
+ partnerPaymentTermsDays;
2397
+ name() {
2398
+ return this.partnerName;
2399
+ }
2400
+ vatId() {
2401
+ return this.partnerVatId;
2402
+ }
2403
+ update(input) {
2404
+ const changes = {};
2405
+ if (typeof input.name === "string" && input.name !== this.partnerName) {
2406
+ changes.name = { from: this.partnerName, to: input.name };
2407
+ this.partnerName = input.name;
2408
+ }
2409
+ if ("vatId" in input && input.vatId !== this.partnerVatId && (typeof input.vatId === "string" || input.vatId === null)) {
2410
+ changes.vatId = { from: this.partnerVatId, to: input.vatId };
2411
+ this.partnerVatId = input.vatId;
2412
+ }
2413
+ if (typeof input.kind === "string" && input.kind !== this.partnerKind) {
2414
+ changes.kind = { from: this.partnerKind, to: input.kind };
2415
+ this.partnerKind = input.kind;
2416
+ }
2417
+ if (typeof input.paymentTermsDays === "number" && input.paymentTermsDays !== this.partnerPaymentTermsDays) {
2418
+ changes.paymentTermsDays = { from: this.partnerPaymentTermsDays, to: input.paymentTermsDays };
2419
+ this.partnerPaymentTermsDays = input.paymentTermsDays;
2420
+ }
2421
+ return changes;
2422
+ }
2423
+ toJSON() {
2424
+ return {
2425
+ id: this.id.value,
2426
+ name: this.partnerName,
2427
+ kind: this.partnerKind,
2428
+ vatId: this.partnerVatId,
2429
+ paymentTermsDays: this.partnerPaymentTermsDays,
2430
+ accountNumbers: this.accountNumbers,
2431
+ address: Object.keys(this.address).length === 0 ? null : this.address
2432
+ };
2433
+ }
2434
+ };
2435
+
2436
+ // src/partner/partner-service.ts
2437
+ function asString3(value) {
2438
+ return typeof value === "string" ? value : null;
2439
+ }
2440
+ var PartnerService = class {
2441
+ constructor(partners, audit, clock, ids) {
2442
+ this.partners = partners;
2443
+ this.audit = audit;
2444
+ this.clock = clock;
2445
+ this.ids = ids;
2446
+ }
2447
+ partners;
2448
+ audit;
2449
+ clock;
2450
+ ids;
2451
+ create(input) {
2452
+ const accountNumbers = (Array.isArray(input.accountNumbers) ? input.accountNumbers : []).filter(
2453
+ (value) => typeof value === "string"
2454
+ );
2455
+ const address = input.address !== null && typeof input.address === "object" && !Array.isArray(input.address) ? input.address : {};
2456
+ const partner = new Partner(
2457
+ this.ids.next(),
2458
+ asString3(input.name) ?? "",
2459
+ asString3(input.kind) ?? "both",
2460
+ asString3(input.vatId),
2461
+ typeof input.paymentTermsDays === "number" ? input.paymentTermsDays : null,
2462
+ accountNumbers,
2463
+ address
2464
+ );
2465
+ this.partners.add(partner);
2466
+ this.recordAudit(input, "created", partner.id, {});
2467
+ return partner;
2468
+ }
2469
+ update(input) {
2470
+ const partner = this.require(input.partnerId);
2471
+ const changes = partner.update(input);
2472
+ if (Object.keys(changes).length > 0) {
2473
+ this.partners.save(partner);
2474
+ this.recordAudit(input, "updated", partner.id, changes);
2475
+ }
2476
+ return partner;
2477
+ }
2478
+ require(partnerId) {
2479
+ let partner = null;
2480
+ if (typeof partnerId === "string" && partnerId !== "") {
2481
+ try {
2482
+ partner = this.partners.byId(Uuid.fromString(partnerId));
2483
+ } catch (error) {
2484
+ if (!(error instanceof InvalidValue)) throw error;
2485
+ }
2486
+ }
2487
+ if (partner === null) {
2488
+ throw new DomainError("E_PARTNER_UNKNOWN", `Gesch\xE4ftspartner ${typeof partnerId === "string" ? partnerId : "?"} existiert nicht`);
2489
+ }
2490
+ return partner;
2491
+ }
2492
+ recordAudit(input, action, objectId, changes) {
2493
+ const actor = asString3(input.actor);
2494
+ this.audit.append(
2495
+ new AuditRecord(
2496
+ this.ids.next(),
2497
+ this.clock.now().toISOString(),
2498
+ actor !== null && actor !== "" ? actor : "system",
2499
+ "partner",
2500
+ objectId,
2501
+ action,
2502
+ changes
2503
+ )
2504
+ );
2505
+ }
2506
+ };
2507
+
2508
+ // src/projection/ec-sales-list.ts
2509
+ var EcSalesListProjection = class {
2510
+ constructor(baseCurrency, journal, vouchers, partners, registry) {
2511
+ this.baseCurrency = baseCurrency;
2512
+ this.journal = journal;
2513
+ this.vouchers = vouchers;
2514
+ this.partners = partners;
2515
+ this.registry = registry;
2516
+ }
2517
+ baseCurrency;
2518
+ journal;
2519
+ vouchers;
2520
+ partners;
2521
+ registry;
2522
+ compute(params) {
2523
+ const year = typeof params.year === "number" ? params.year : 0;
2524
+ const quarter = typeof params.quarter === "number" ? params.quarter : 0;
2525
+ const intraCommunityKeys = /* @__PURE__ */ new Set();
2526
+ for (const version of this.registry.allVersions()) {
2527
+ if (version.mechanism === "intra_community_supply" && version.reportingKey !== null) {
2528
+ intraCommunityKeys.add(version.reportingKey);
2529
+ }
2530
+ }
2531
+ const byVatId = /* @__PURE__ */ new Map();
2532
+ for (const entry of this.journal.all()) {
2533
+ const voucher = this.vouchers.byId(entry.voucherId);
2534
+ const taxDate = voucher === null ? entry.entryDate : voucher.taxDate();
2535
+ if (taxDate.year() !== year) continue;
2536
+ if (quarter !== 0 && Math.floor((taxDate.month() - 1) / 3) + 1 !== quarter) continue;
2537
+ const partner = voucher?.partnerId == null ? null : this.partners.byId(voucher.partnerId);
2538
+ const vatId = partner?.vatId() ?? null;
2539
+ if (vatId === null) continue;
2540
+ for (const line of entry.lines()) {
2541
+ const rawKey = line.taxTag?.reportingKey;
2542
+ if (typeof rawKey !== "string" && typeof rawKey !== "number") continue;
2543
+ if (!intraCommunityKeys.has(String(rawKey))) continue;
2544
+ const signed = line.side === "credit" ? line.money : line.money.negate();
2545
+ byVatId.set(vatId, (byVatId.get(vatId) ?? Money.zero(this.baseCurrency)).add(signed));
2546
+ }
2547
+ }
2548
+ const vatIds = [...byVatId.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2549
+ const rows = [];
2550
+ for (const vatId of vatIds) {
2551
+ const amount = byVatId.get(vatId);
2552
+ if (amount.isZero()) continue;
2553
+ rows.push({ vatId, amount: amount.amountAsString(), kind: "supply" });
2554
+ }
2555
+ return { rows };
2556
+ }
2557
+ };
2558
+
2559
+ // src/projection/trial-balance.ts
2560
+ var TrialBalanceProjection = class {
2561
+ constructor(baseCurrency, accounts, journal) {
2562
+ this.baseCurrency = baseCurrency;
2563
+ this.accounts = accounts;
2564
+ this.journal = journal;
2565
+ }
2566
+ baseCurrency;
2567
+ accounts;
2568
+ journal;
2569
+ compute(params) {
2570
+ const fiscalYear = typeof params.fiscalYear === "number" ? params.fiscalYear : 0;
2571
+ const throughPeriod = typeof params.throughPeriod === "number" ? params.throughPeriod : Number.MAX_SAFE_INTEGER;
2572
+ const includeZeroBalances = params.includeZeroBalances === true;
2573
+ const zero = Money.zero(this.baseCurrency);
2574
+ const totals = /* @__PURE__ */ new Map();
2575
+ for (const entry of this.journal.all()) {
2576
+ const entryYear = entry.periodRef.fiscalYear;
2577
+ const entryPeriod = entry.periodRef.period;
2578
+ const isPriorYear = entryYear < fiscalYear;
2579
+ const isCurrentScope = entryYear === fiscalYear && entryPeriod <= throughPeriod;
2580
+ if (!isPriorYear && !isCurrentScope) continue;
2581
+ for (const line of entry.lines()) {
2582
+ const account = this.accounts.byId(line.accountId);
2583
+ if (account === null) continue;
2584
+ if (isPriorYear && !isBalanceCarrying(account.type)) continue;
2585
+ const key = account.number.value;
2586
+ let total = totals.get(key);
2587
+ if (total === void 0) {
2588
+ total = { opening: zero, debit: zero, credit: zero, touched: false };
2589
+ totals.set(key, total);
2590
+ }
2591
+ if (isPriorYear) {
2592
+ total.opening = line.side === "debit" ? total.opening.add(line.money) : total.opening.subtract(line.money);
2593
+ continue;
2594
+ }
2595
+ if (line.side === "debit") total.debit = total.debit.add(line.money);
2596
+ else total.credit = total.credit.add(line.money);
2597
+ total.touched = true;
2598
+ }
2599
+ }
2600
+ if (includeZeroBalances) {
2601
+ for (const account of this.accounts.all()) {
2602
+ if (!totals.has(account.number.value)) {
2603
+ totals.set(account.number.value, { opening: zero, debit: zero, credit: zero, touched: false });
2604
+ }
2605
+ }
2606
+ }
2607
+ const numbers = [...totals.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2608
+ const rows = [];
2609
+ for (const number of numbers) {
2610
+ const total = totals.get(number);
2611
+ const balance = total.opening.add(total.debit).subtract(total.credit);
2612
+ if (!includeZeroBalances && balance.isZero() && !total.touched) continue;
2613
+ rows.push({
2614
+ account: number,
2615
+ openingBalance: total.opening.amountAsString(),
2616
+ debitTotal: total.debit.amountAsString(),
2617
+ creditTotal: total.credit.amountAsString(),
2618
+ balance: balance.amountAsString()
2619
+ });
2620
+ }
2621
+ return { rows };
2622
+ }
2623
+ };
2624
+
2625
+ // src/projection/open-items.ts
2626
+ var OpenItemsProjection = class {
2627
+ constructor(openItems, vouchers, journal) {
2628
+ this.openItems = openItems;
2629
+ this.vouchers = vouchers;
2630
+ this.journal = journal;
2631
+ }
2632
+ openItems;
2633
+ vouchers;
2634
+ journal;
2635
+ compute(params) {
2636
+ const asOf = typeof params.asOf === "string" ? CalendarDate.of(params.asOf) : null;
2637
+ const kind = parseOpenItemKind(params.kind);
2638
+ const partnerId = typeof params.partnerId === "string" ? params.partnerId : null;
2639
+ const open = this.openItems.all().filter((item) => {
2640
+ if (kind !== null && item.kind !== kind) return false;
2641
+ if (partnerId !== null && (item.partnerId?.value ?? null) !== partnerId) return false;
2642
+ if (asOf !== null && item.openedAt.isAfter(asOf)) return false;
2643
+ if (item.remainingAt(asOf).isZero()) return false;
2644
+ return true;
2645
+ });
2646
+ open.sort((a, b) => {
2647
+ const byDate = this.voucherDate(a).compareTo(this.voucherDate(b));
2648
+ return byDate !== 0 ? byDate : this.sequenceNumber(a) - this.sequenceNumber(b);
2649
+ });
2650
+ return { items: open.map((item) => this.serializeItem(item, asOf)) };
2651
+ }
2652
+ voucherDate(item) {
2653
+ return this.vouchers.byId(item.voucherId)?.voucherDate ?? item.openedAt;
2654
+ }
2655
+ sequenceNumber(item) {
2656
+ return this.journal.byId(item.originEntryId)?.sequenceNumber ?? 0;
2657
+ }
2658
+ serializeItem(item, asOf) {
2659
+ return {
2660
+ id: item.id.value,
2661
+ kind: item.kind,
2662
+ voucherNumber: this.vouchers.byId(item.voucherId)?.voucherNumber ?? null,
2663
+ money: item.money.toJSON(),
2664
+ remaining: item.remainingAt(asOf).toJSON(),
2665
+ status: item.statusAt(asOf)
2666
+ };
2667
+ }
2668
+ };
2669
+
2670
+ // src/projection/account-sheet.ts
2671
+ var AccountSheetProjection = class {
2672
+ constructor(baseCurrency, accounts, journal) {
2673
+ this.baseCurrency = baseCurrency;
2674
+ this.accounts = accounts;
2675
+ this.journal = journal;
2676
+ }
2677
+ baseCurrency;
2678
+ accounts;
2679
+ journal;
2680
+ compute(params) {
2681
+ const number = typeof params.account === "string" ? params.account : "";
2682
+ const fiscalYear = typeof params.fiscalYear === "number" ? params.fiscalYear : 0;
2683
+ const throughPeriod = typeof params.throughPeriod === "number" ? params.throughPeriod : Number.MAX_SAFE_INTEGER;
2684
+ const account = this.accounts.byNumber(AccountNumber.of(number));
2685
+ if (account === null) {
2686
+ throw new DomainError("E_ACCOUNT_UNKNOWN", `Konto ${number} existiert nicht`);
2687
+ }
2688
+ let opening = Money.zero(this.baseCurrency);
2689
+ if (isBalanceCarrying(account.type)) {
2690
+ for (const entry of this.journal.all()) {
2691
+ if (entry.periodRef.fiscalYear >= fiscalYear) continue;
2692
+ for (const line of entry.lines()) {
2693
+ if (!line.accountId.equals(account.id)) continue;
2694
+ opening = line.side === "debit" ? opening.add(line.money) : opening.subtract(line.money);
2695
+ }
2696
+ }
2697
+ }
2698
+ let running = opening;
2699
+ const lines = [];
2700
+ for (const entry of this.journal.forFiscalYear(fiscalYear)) {
2701
+ if (entry.periodRef.period > throughPeriod) continue;
2702
+ for (const line of entry.lines()) {
2703
+ if (!line.accountId.equals(account.id)) continue;
2704
+ running = line.side === "debit" ? running.add(line.money) : running.subtract(line.money);
2705
+ lines.push({
2706
+ sequenceNumber: entry.sequenceNumber,
2707
+ entryDate: entry.entryDate.iso,
2708
+ text: entry.text(),
2709
+ side: line.side,
2710
+ money: line.money.toJSON(),
2711
+ runningBalance: running.amountAsString()
2712
+ });
2713
+ }
2714
+ }
2715
+ return {
2716
+ account: account.number.value,
2717
+ name: account.name,
2718
+ openingBalance: opening.amountAsString(),
2719
+ lines,
2720
+ closingBalance: running.amountAsString()
2721
+ };
2722
+ }
2723
+ };
2724
+
2725
+ // src/projection/audit-log.ts
2726
+ var AuditLogProjection = class {
2727
+ constructor(audit) {
2728
+ this.audit = audit;
2729
+ }
2730
+ audit;
2731
+ compute(params) {
2732
+ const from = typeof params.from === "string" ? CalendarDate.of(params.from) : null;
2733
+ const to = typeof params.to === "string" ? CalendarDate.of(params.to) : null;
2734
+ const records = [];
2735
+ for (const record of this.audit.all()) {
2736
+ const date = CalendarDate.of(record.at.slice(0, 10));
2737
+ if (from !== null && date.isBefore(from)) continue;
2738
+ if (to !== null && date.isAfter(to)) continue;
2739
+ records.push(record.toJSON());
2740
+ }
2741
+ return { records };
2742
+ }
2743
+ };
2744
+ var VatReturnProjection = class {
2745
+ constructor(baseCurrency, journal, openItems, vouchers, accounts, registry, profile) {
2746
+ this.baseCurrency = baseCurrency;
2747
+ this.journal = journal;
2748
+ this.openItems = openItems;
2749
+ this.vouchers = vouchers;
2750
+ this.accounts = accounts;
2751
+ this.registry = registry;
2752
+ this.profile = profile;
2753
+ }
2754
+ baseCurrency;
2755
+ journal;
2756
+ openItems;
2757
+ vouchers;
2758
+ accounts;
2759
+ registry;
2760
+ profile;
2761
+ compute(params) {
2762
+ const year = typeof params.year === "number" ? params.year : 0;
2763
+ const quarter = typeof params.quarter === "number" ? params.quarter : 0;
2764
+ const asOf = typeof params.asOf === "string" ? CalendarDate.of(params.asOf) : null;
2765
+ const zero = Money.zero(this.baseCurrency);
2766
+ const keys = /* @__PURE__ */ new Map();
2767
+ const directions = this.registryDirections();
2768
+ const add = (key, base, tax) => {
2769
+ const current = keys.get(key) ?? { base: zero, tax: zero };
2770
+ keys.set(key, { base: current.base.add(base), tax: current.tax.add(tax) });
2771
+ };
2772
+ if (this.profile.isCashBasis()) {
2773
+ for (const item of this.openItems.all()) {
2774
+ const origin = this.journal.byId(item.originEntryId);
2775
+ if (origin === null || asOf !== null && origin.entryDate.isAfter(asOf)) continue;
2776
+ const contributions = this.entryContributions(origin, directions);
2777
+ if (contributions.size === 0) continue;
2778
+ for (const share of this.allocateToSettlements(item, contributions)) {
2779
+ if (asOf !== null && share.settledAt.isAfter(asOf)) continue;
2780
+ if (this.inQuarter(share.settledAt, year, quarter)) add(share.key, share.base, share.tax);
2781
+ }
2782
+ }
2783
+ for (const entry of this.journal.all()) {
2784
+ if (!this.inQuarter(entry.entryDate, year, quarter)) continue;
2785
+ if (asOf !== null && entry.entryDate.isAfter(asOf)) continue;
2786
+ if (this.openItems.byOriginEntry(entry.id).length > 0) continue;
2787
+ for (const [key, contribution] of this.entryContributions(entry, directions)) {
2788
+ add(key, contribution.base, contribution.tax);
2789
+ }
2790
+ }
2791
+ } else {
2792
+ for (const entry of this.journal.all()) {
2793
+ let taxDate;
2794
+ if (entry.reverses !== null) {
2795
+ taxDate = entry.entryDate;
2796
+ } else {
2797
+ const voucher = this.vouchers.byId(entry.voucherId);
2798
+ taxDate = voucher === null ? entry.entryDate : voucher.taxDate();
2799
+ }
2800
+ if (!this.inQuarter(taxDate, year, quarter)) continue;
2801
+ if (asOf !== null && entry.entryDate.isAfter(asOf)) continue;
2802
+ for (const [key, contribution] of this.entryContributions(entry, directions)) {
2803
+ add(key, contribution.base, contribution.tax);
2804
+ }
2805
+ }
2806
+ }
2807
+ const sortedKeys = [...keys.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2808
+ const result = {};
2809
+ let payload = zero;
2810
+ for (const key of sortedKeys) {
2811
+ const amounts = keys.get(key);
2812
+ const flooredBase = Money.fromCalculation(
2813
+ new Big(amounts.base.amountAsString()).round(0, Big.roundDown),
2814
+ this.baseCurrency
2815
+ );
2816
+ result[key] = { base: flooredBase.amountAsString(), tax: amounts.tax.amountAsString() };
2817
+ const direction = directions.get(key) ?? "output";
2818
+ payload = direction === "input" ? payload.subtract(amounts.tax) : payload.add(amounts.tax);
2819
+ }
2820
+ return { keys: result, payload: payload.toJSON() };
2821
+ }
2822
+ registryDirections() {
2823
+ const directions = /* @__PURE__ */ new Map();
2824
+ for (const version of this.registry.allVersions()) {
2825
+ if (version.reportingKey !== null) {
2826
+ directions.set(version.reportingKey, this.accountDirection(version.taxAccount));
2827
+ }
2828
+ if (version.inputReportingKey !== null) {
2829
+ directions.set(version.inputReportingKey, "input");
2830
+ }
2831
+ if (version.baseReportingKey !== null) {
2832
+ directions.set(
2833
+ version.baseReportingKey,
2834
+ version.mechanism === "reverse_charge" ? "input" : this.accountDirection(version.taxAccount)
2835
+ );
2836
+ }
2837
+ }
2838
+ return directions;
2839
+ }
2840
+ accountDirection(accountNumber) {
2841
+ if (accountNumber === "") return "output";
2842
+ const account = this.accounts.byNumber(AccountNumber.of(accountNumber));
2843
+ return account?.subtype === "tax_in" ? "input" : "output";
2844
+ }
2845
+ entryContributions(entry, directions) {
2846
+ const zero = Money.zero(this.baseCurrency);
2847
+ const collected = /* @__PURE__ */ new Map();
2848
+ for (const line of entry.lines()) {
2849
+ const tag = line.taxTag;
2850
+ if (tag === null) continue;
2851
+ const rawKey = tag.reportingKey;
2852
+ if (typeof rawKey !== "string" && typeof rawKey !== "number") continue;
2853
+ const key = String(rawKey);
2854
+ const account = this.accounts.byId(line.accountId);
2855
+ const subtype = account?.subtype ?? null;
2856
+ const entryFor = collected.get(key) ?? {
2857
+ baseFromTax: zero,
2858
+ hasTaxBase: false,
2859
+ baseFallback: zero,
2860
+ tax: zero
2861
+ };
2862
+ if (subtype === "tax_out" || subtype === "tax_in") {
2863
+ const positiveSide = subtype === "tax_out" ? "credit" : "debit";
2864
+ const signed = line.side === positiveSide ? line.money : line.money.negate();
2865
+ entryFor.tax = entryFor.tax.add(signed);
2866
+ let baseMoney = this.tagBaseMoney(tag);
2867
+ if (baseMoney !== null) {
2868
+ if (line.money.isNegative()) baseMoney = baseMoney.negate();
2869
+ entryFor.baseFromTax = entryFor.baseFromTax.add(baseMoney);
2870
+ entryFor.hasTaxBase = true;
2871
+ }
2872
+ } else {
2873
+ const direction = directions.get(key) ?? "output";
2874
+ const positiveSide = direction === "input" ? "debit" : "credit";
2875
+ const signed = line.side === positiveSide ? line.money : line.money.negate();
2876
+ entryFor.baseFallback = entryFor.baseFallback.add(signed);
2877
+ }
2878
+ collected.set(key, entryFor);
2879
+ }
2880
+ const contributions = /* @__PURE__ */ new Map();
2881
+ for (const [key, parts] of collected) {
2882
+ const base = parts.hasTaxBase ? parts.baseFromTax : parts.baseFallback;
2883
+ if (base.isZero() && parts.tax.isZero()) continue;
2884
+ contributions.set(key, { base, tax: parts.tax });
2885
+ }
2886
+ return contributions;
2887
+ }
2888
+ tagBaseMoney(tag) {
2889
+ const baseMoney = tag.baseMoney;
2890
+ const amount = baseMoney !== null && typeof baseMoney === "object" && typeof baseMoney.amount === "string" ? baseMoney.amount : null;
2891
+ return amount === null ? null : Money.of(amount, this.baseCurrency);
2892
+ }
2893
+ allocateToSettlements(item, contributions) {
2894
+ const shares = [];
2895
+ const allocated = /* @__PURE__ */ new Map();
2896
+ let remaining = item.money;
2897
+ const total = new Big(item.money.amountAsString());
2898
+ for (const settlement of item.settlements()) {
2899
+ remaining = remaining.subtract(settlement.money);
2900
+ const isFinal = remaining.isZero();
2901
+ const ratio = new Big(settlement.money.amountAsString());
2902
+ for (const [key, contribution] of contributions) {
2903
+ const current = allocated.get(key) ?? {
2904
+ base: Money.zero(this.baseCurrency),
2905
+ tax: Money.zero(this.baseCurrency)
2906
+ };
2907
+ let base;
2908
+ let tax;
2909
+ if (isFinal) {
2910
+ base = contribution.base.subtract(current.base);
2911
+ tax = contribution.tax.subtract(current.tax);
2912
+ } else {
2913
+ base = this.proportional(contribution.base, ratio, total);
2914
+ tax = this.proportional(contribution.tax, ratio, total);
2915
+ }
2916
+ allocated.set(key, { base: current.base.add(base), tax: current.tax.add(tax) });
2917
+ shares.push({ key, base, tax, settledAt: settlement.settledAt });
2918
+ }
2919
+ }
2920
+ return shares;
2921
+ }
2922
+ proportional(total, part, whole) {
2923
+ if (whole.eq(0)) return Money.zero(this.baseCurrency);
2924
+ return Money.fromCalculation(
2925
+ new Big(total.amountAsString()).times(part).div(whole),
2926
+ this.baseCurrency
2927
+ );
2928
+ }
2929
+ inQuarter(date, year, quarter) {
2930
+ if (date.year() !== year) return false;
2931
+ return quarter === 0 || Math.floor((date.month() - 1) / 3) + 1 === quarter;
2932
+ }
2933
+ };
2934
+
2935
+ // src/projection/income-statement.ts
2936
+ var IncomeStatementProjection = class {
2937
+ constructor(baseCurrency, accounts, journal, mappings) {
2938
+ this.baseCurrency = baseCurrency;
2939
+ this.accounts = accounts;
2940
+ this.journal = journal;
2941
+ this.mappings = mappings;
2942
+ }
2943
+ baseCurrency;
2944
+ accounts;
2945
+ journal;
2946
+ mappings;
2947
+ compute(params) {
2948
+ const fiscalYear = typeof params.fiscalYear === "number" ? params.fiscalYear : 0;
2949
+ const fromPeriod = typeof params.fromPeriod === "number" ? params.fromPeriod : 1;
2950
+ const throughPeriod = typeof params.throughPeriod === "number" ? params.throughPeriod : Number.MAX_SAFE_INTEGER;
2951
+ const mappingId = typeof params.mapping === "string" ? params.mapping : "";
2952
+ const mapping = this.mappings.byId(mappingId);
2953
+ if (mapping === null) {
2954
+ throw new DomainError("E_MAPPING_OVERLAP", `Mapping "${mappingId}" ist nicht geladen`);
2955
+ }
2956
+ const zero = Money.zero(this.baseCurrency);
2957
+ const amounts = /* @__PURE__ */ new Map();
2958
+ const touched = /* @__PURE__ */ new Set();
2959
+ for (const entry of this.journal.forFiscalYear(fiscalYear)) {
2960
+ const period = entry.periodRef.period;
2961
+ if (period < fromPeriod || period > throughPeriod) continue;
2962
+ for (const line of entry.lines()) {
2963
+ const account = this.accounts.byId(line.accountId);
2964
+ if (account === null || isBalanceCarrying(account.type)) continue;
2965
+ const leaf = mapping.leafFor(account.number.value);
2966
+ if (leaf === null) continue;
2967
+ const signed = line.side === "credit" ? line.money : line.money.negate();
2968
+ amounts.set(leaf.key, (amounts.get(leaf.key) ?? zero).add(signed));
2969
+ touched.add(leaf.key);
2970
+ }
2971
+ }
2972
+ const positions = [];
2973
+ let netIncome = zero;
2974
+ for (const leaf of mapping.leaves) {
2975
+ const amount = amounts.get(leaf.key) ?? zero;
2976
+ netIncome = netIncome.add(amount);
2977
+ if (amount.isZero() && !touched.has(leaf.key)) continue;
2978
+ positions.push({ key: leaf.key, label: leaf.label, amount: amount.amountAsString() });
2979
+ }
2980
+ return { positions, netIncome: netIncome.amountAsString() };
2981
+ }
2982
+ };
2983
+
2984
+ // src/mapping/mapping.ts
2985
+ function isRecord4(value) {
2986
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2987
+ }
2988
+ function asString4(value) {
2989
+ return typeof value === "string" ? value : null;
2990
+ }
2991
+ function leafMatches(leaf, accountNumber) {
2992
+ if (leaf.numbers.includes(accountNumber)) return true;
2993
+ for (const range of leaf.ranges) {
2994
+ if (accountNumber >= range.from && accountNumber <= range.to) return true;
2995
+ }
2996
+ return false;
2997
+ }
2998
+ var Mapping = class _Mapping {
2999
+ constructor(id, kind, version, leaves) {
3000
+ this.id = id;
3001
+ this.kind = kind;
3002
+ this.version = version;
3003
+ this.leaves = leaves;
3004
+ }
3005
+ id;
3006
+ kind;
3007
+ version;
3008
+ leaves;
3009
+ static fromData(data) {
3010
+ const leaves = [];
3011
+ _Mapping.collectLeaves(Array.isArray(data.positions) ? data.positions : [], [], leaves, null);
3012
+ return new _Mapping(asString4(data.id) ?? "", asString4(data.kind) ?? "", asString4(data.version) ?? "", leaves);
3013
+ }
3014
+ static collectLeaves(positions, parents, leaves, side) {
3015
+ for (const position of positions) {
3016
+ if (!isRecord4(position)) continue;
3017
+ const key = asString4(position.key) ?? "";
3018
+ const nodeSide = asString4(position.side) ?? side;
3019
+ const children = Array.isArray(position.children) ? position.children : [];
3020
+ if (children.length > 0) {
3021
+ _Mapping.collectLeaves(children, [...parents, key], leaves, nodeSide);
3022
+ continue;
3023
+ }
3024
+ const ranges = [];
3025
+ const numbers = [];
3026
+ for (const selector of Array.isArray(position.accounts) ? position.accounts : []) {
3027
+ if (!isRecord4(selector)) continue;
3028
+ if (typeof selector.from === "string" && typeof selector.to === "string") {
3029
+ ranges.push({ from: selector.from, to: selector.to });
3030
+ }
3031
+ for (const number of Array.isArray(selector.numbers) ? selector.numbers : []) {
3032
+ if (typeof number === "string") numbers.push(number);
3033
+ }
3034
+ }
3035
+ leaves.push({
3036
+ key,
3037
+ label: asString4(position.label) ?? key,
3038
+ side: nodeSide,
3039
+ ranges,
3040
+ numbers,
3041
+ includeNonCash: position.includeNonCash === true,
3042
+ includesNetIncome: position.includesNetIncome === true,
3043
+ parents
3044
+ });
3045
+ }
3046
+ }
3047
+ leafFor(accountNumber) {
3048
+ for (const leaf of this.leaves) {
3049
+ if (leafMatches(leaf, accountNumber)) return leaf;
3050
+ }
3051
+ return null;
3052
+ }
3053
+ };
3054
+
3055
+ // src/projection/balance-sheet.ts
3056
+ var BalanceSheetProjection = class {
3057
+ constructor(baseCurrency, accounts, journal, mappings) {
3058
+ this.baseCurrency = baseCurrency;
3059
+ this.accounts = accounts;
3060
+ this.journal = journal;
3061
+ this.mappings = mappings;
3062
+ }
3063
+ baseCurrency;
3064
+ accounts;
3065
+ journal;
3066
+ mappings;
3067
+ compute(params) {
3068
+ const asOf = typeof params.asOf === "string" ? CalendarDate.of(params.asOf) : null;
3069
+ const mappingId = typeof params.mapping === "string" ? params.mapping : "";
3070
+ const mapping = this.mappings.byId(mappingId);
3071
+ if (mapping === null) {
3072
+ throw new DomainError("E_MAPPING_OVERLAP", `Mapping "${mappingId}" ist nicht geladen`);
3073
+ }
3074
+ const zero = Money.zero(this.baseCurrency);
3075
+ const debits = /* @__PURE__ */ new Map();
3076
+ const credits = /* @__PURE__ */ new Map();
3077
+ const touchedAccounts = /* @__PURE__ */ new Set();
3078
+ let netIncome = zero;
3079
+ for (const entry of this.journal.all()) {
3080
+ if (asOf !== null && entry.entryDate.isAfter(asOf)) continue;
3081
+ for (const line of entry.lines()) {
3082
+ const account = this.accounts.byId(line.accountId);
3083
+ if (account === null) continue;
3084
+ if (!isBalanceCarrying(account.type)) {
3085
+ netIncome = line.side === "credit" ? netIncome.add(line.money) : netIncome.subtract(line.money);
3086
+ continue;
3087
+ }
3088
+ const key = account.number.value;
3089
+ if (line.side === "debit") debits.set(key, (debits.get(key) ?? zero).add(line.money));
3090
+ else credits.set(key, (credits.get(key) ?? zero).add(line.money));
3091
+ touchedAccounts.add(key);
3092
+ }
3093
+ }
3094
+ const allNumbers = /* @__PURE__ */ new Set([...debits.keys(), ...credits.keys()]);
3095
+ const sections = { assets: [], liabilitiesAndEquity: [] };
3096
+ const totals = { assets: zero, liabilitiesAndEquity: zero };
3097
+ for (const leaf of mapping.leaves) {
3098
+ const section = leaf.side === "liabilitiesAndEquity" ? "liabilitiesAndEquity" : "assets";
3099
+ let amount = zero;
3100
+ let touched = false;
3101
+ for (const number of allNumbers) {
3102
+ if (!leafMatches(leaf, number)) continue;
3103
+ const debit = debits.get(number) ?? zero;
3104
+ const credit = credits.get(number) ?? zero;
3105
+ amount = section === "assets" ? amount.add(debit).subtract(credit) : amount.add(credit).subtract(debit);
3106
+ touched = touched || touchedAccounts.has(number);
3107
+ }
3108
+ if (leaf.includesNetIncome) {
3109
+ amount = amount.add(netIncome);
3110
+ touched = touched || !netIncome.isZero();
3111
+ }
3112
+ if (amount.isZero() && !touched) continue;
3113
+ sections[section].push({ key: leaf.key, label: leaf.label, amount: amount.amountAsString() });
3114
+ totals[section] = totals[section].add(amount);
3115
+ }
3116
+ return {
3117
+ assets: sections.assets,
3118
+ assetsTotal: totals.assets.amountAsString(),
3119
+ liabilitiesAndEquity: sections.liabilitiesAndEquity,
3120
+ liabilitiesAndEquityTotal: totals.liabilitiesAndEquity.amountAsString()
3121
+ };
3122
+ }
3123
+ };
3124
+ var NON_PROFIT_SUBTYPES = /* @__PURE__ */ new Set(["bank", "cash", "transit", "ar", "ap"]);
3125
+ var CashBasisProjection = class {
3126
+ constructor(baseCurrency, accounts, journal, openItems, vouchers, fiscalYears, mappings) {
3127
+ this.baseCurrency = baseCurrency;
3128
+ this.accounts = accounts;
3129
+ this.journal = journal;
3130
+ this.openItems = openItems;
3131
+ this.vouchers = vouchers;
3132
+ this.fiscalYears = fiscalYears;
3133
+ this.mappings = mappings;
3134
+ }
3135
+ baseCurrency;
3136
+ accounts;
3137
+ journal;
3138
+ openItems;
3139
+ vouchers;
3140
+ fiscalYears;
3141
+ mappings;
3142
+ compute(params) {
3143
+ const year = typeof params.year === "number" ? params.year : 0;
3144
+ const asOf = typeof params.asOf === "string" ? CalendarDate.of(params.asOf) : null;
3145
+ const mapping = typeof params.mapping === "string" ? this.mappings.byId(params.mapping) : null;
3146
+ this.assertCalendarYearFiscalYears(year);
3147
+ const income = /* @__PURE__ */ new Map();
3148
+ const expenses = /* @__PURE__ */ new Map();
3149
+ const addTo = (bucket, label, amount) => {
3150
+ bucket.set(label, (bucket.get(label) ?? Money.zero(this.baseCurrency)).add(amount));
3151
+ };
3152
+ for (const entry of this.journal.all()) {
3153
+ if (asOf !== null && entry.entryDate.isAfter(asOf)) continue;
3154
+ const bankFlow = this.bankFlow(entry);
3155
+ if (bankFlow.isZero()) {
3156
+ if (mapping === null || entry.entryDate.year() !== year) continue;
3157
+ for (const line of entry.lines()) {
3158
+ const account = this.accounts.byId(line.accountId);
3159
+ if (account === null) continue;
3160
+ const leaf = mapping.leafFor(account.number.value);
3161
+ if (leaf === null || !leaf.includeNonCash) continue;
3162
+ if (account.type === "revenue" || account.subtype === "tax_out") {
3163
+ const signed = line.side === "credit" ? line.money : line.money.negate();
3164
+ addTo(income, leaf.label, signed);
3165
+ } else if (account.type === "expense" || account.subtype === "tax_in") {
3166
+ const signed = line.side === "debit" ? line.money : line.money.negate();
3167
+ addTo(expenses, leaf.label, signed);
3168
+ }
3169
+ }
3170
+ continue;
3171
+ }
3172
+ if (this.assignYear(entry) !== year) continue;
3173
+ const inflow = bankFlow.isPositive();
3174
+ for (const sourced of this.sourceLines(entry)) {
3175
+ const account = this.accounts.byId(sourced.line.accountId);
3176
+ if (account === null || account.subtype !== null && NON_PROFIT_SUBTYPES.has(account.subtype)) {
3177
+ continue;
3178
+ }
3179
+ const amount = this.proportional(sourced.line.money, sourced.ratio);
3180
+ if (account.subtype === "tax_out") {
3181
+ addTo(inflow ? income : expenses, inflow ? "Vereinnahmte USt" : "USt-Zahlung an FA", amount);
3182
+ } else if (account.subtype === "tax_in") {
3183
+ addTo(expenses, "Gezahlte Vorsteuer", amount);
3184
+ } else if (account.type === "revenue") {
3185
+ addTo(income, this.label(mapping, account), amount);
3186
+ } else if (account.type === "expense") {
3187
+ addTo(expenses, this.label(mapping, account), amount);
3188
+ }
3189
+ }
3190
+ }
3191
+ return { income: this.serializeBucket(income), expenses: this.serializeBucket(expenses) };
3192
+ }
3193
+ assertCalendarYearFiscalYears(year) {
3194
+ const y = String(year).padStart(4, "0");
3195
+ const start = CalendarDate.of(`${y}-01-01`);
3196
+ const end = CalendarDate.of(`${y}-12-31`);
3197
+ for (const fiscalYear of this.fiscalYears.all()) {
3198
+ const overlaps = !fiscalYear.end.isBefore(start) && !fiscalYear.start.isAfter(end);
3199
+ if (!overlaps) continue;
3200
+ const isCalendarYear = fiscalYear.start.iso.slice(5) === "01-01" && fiscalYear.end.iso.slice(5) === "12-31";
3201
+ if (!isCalendarYear) {
3202
+ throw new DomainError(
3203
+ "E_CASHBASIS_DEVIATING_FISCAL_YEAR",
3204
+ `Gesch\xE4ftsjahr ${fiscalYear.year} (${fiscalYear.start.iso} bis ${fiscalYear.end.iso}) weicht vom Kalenderjahr ab \u2014 E\xDCR ist kalenderjahrgebunden`,
3205
+ { fiscalYear: fiscalYear.year }
3206
+ );
3207
+ }
3208
+ }
3209
+ }
3210
+ bankFlow(entry) {
3211
+ let flow = Money.zero(this.baseCurrency);
3212
+ for (const line of entry.lines()) {
3213
+ const account = this.accounts.byId(line.accountId);
3214
+ const subtype = account?.subtype ?? null;
3215
+ if (subtype !== "bank" && subtype !== "cash") continue;
3216
+ flow = line.side === "debit" ? flow.add(line.money) : flow.subtract(line.money);
3217
+ }
3218
+ return flow;
3219
+ }
3220
+ assignYear(entry) {
3221
+ const voucher = this.vouchers.byId(entry.voucherId);
3222
+ if (voucher !== null && voucher.recurring && voucher.economicYear !== null && voucher.due !== null && this.inTenDayWindow(entry.entryDate) && this.inTenDayWindow(voucher.due)) {
3223
+ return voucher.economicYear;
3224
+ }
3225
+ return entry.entryDate.year();
3226
+ }
3227
+ inTenDayWindow(date) {
3228
+ const month = date.month();
3229
+ const day = Number(date.iso.slice(8, 10));
3230
+ return month === 12 && day >= 22 || month === 1 && day <= 10;
3231
+ }
3232
+ sourceLines(entry) {
3233
+ const sourced = [];
3234
+ for (const item of this.openItems.all()) {
3235
+ for (const settlement of item.settlements()) {
3236
+ if (!settlement.entryId.equals(entry.id)) continue;
3237
+ const origin = this.journal.byId(item.originEntryId);
3238
+ if (origin === null || item.money.isZero()) continue;
3239
+ const ratio = new Big(settlement.money.amountAsString()).div(new Big(item.money.amountAsString()));
3240
+ for (const line of origin.lines()) sourced.push({ line, ratio });
3241
+ }
3242
+ }
3243
+ if (sourced.length > 0) return sourced;
3244
+ const one = new Big(1);
3245
+ for (const line of entry.lines()) sourced.push({ line, ratio: one });
3246
+ return sourced;
3247
+ }
3248
+ proportional(amount, ratio) {
3249
+ if (ratio.eq(1)) return amount.abs();
3250
+ return Money.fromCalculation(new Big(amount.abs().amountAsString()).times(ratio), this.baseCurrency);
3251
+ }
3252
+ label(mapping, account) {
3253
+ if (mapping !== null) {
3254
+ const leaf = mapping.leafFor(account.number.value);
3255
+ if (leaf !== null) return leaf.label;
3256
+ }
3257
+ return account.name;
3258
+ }
3259
+ serializeBucket(bucket) {
3260
+ const labels = [...bucket.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3261
+ const rows = [];
3262
+ for (const label of labels) {
3263
+ const amount = bucket.get(label);
3264
+ if (amount.isZero()) continue;
3265
+ rows.push({ category: label, amount: amount.amountAsString() });
3266
+ }
3267
+ return rows;
3268
+ }
3269
+ };
3270
+
3271
+ // src/projection/asset-register.ts
3272
+ var AssetRegisterProjection = class {
3273
+ constructor(assets) {
3274
+ this.assets = assets;
3275
+ }
3276
+ assets;
3277
+ compute(params) {
3278
+ const asOf = typeof params.asOf === "string" ? CalendarDate.of(params.asOf) : null;
3279
+ const sorted = [...this.assets.all()].sort((a, b) => {
3280
+ const byDate = a.acquiredOn.compareTo(b.acquiredOn);
3281
+ return byDate !== 0 ? byDate : a.id.compareTo(b.id);
3282
+ });
3283
+ const rows = [];
3284
+ for (const asset of sorted) {
3285
+ if (asOf !== null && asset.acquiredOn.isAfter(asOf)) continue;
3286
+ const row = asset.toJSON();
3287
+ row.accumulatedDepreciation = asset.accumulatedDepreciationAt(asOf).toJSON();
3288
+ row.bookValue = asset.bookValueAt(asOf).toJSON();
3289
+ if (asset.route === "capitalize") row.depreciationSchedule = asset.scheduleSummary();
3290
+ rows.push(row);
3291
+ }
3292
+ return { assets: rows };
3293
+ }
3294
+ };
3295
+ var FORMAT_VERSION = "0.4";
3296
+ var LINE_FIELDS = ["accountId", "side", "money", "dimensions", "taxTag"];
3297
+ function withoutNulls(row) {
3298
+ return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null));
3299
+ }
3300
+ var JournalExportProjection = class _JournalExportProjection {
3301
+ constructor(tenantId, tenantName, baseCurrency, journal, accounts, vouchers, partners, audit, clock) {
3302
+ this.tenantId = tenantId;
3303
+ this.tenantName = tenantName;
3304
+ this.baseCurrency = baseCurrency;
3305
+ this.journal = journal;
3306
+ this.accounts = accounts;
3307
+ this.vouchers = vouchers;
3308
+ this.partners = partners;
3309
+ this.audit = audit;
3310
+ this.clock = clock;
3311
+ }
3312
+ tenantId;
3313
+ tenantName;
3314
+ baseCurrency;
3315
+ journal;
3316
+ accounts;
3317
+ vouchers;
3318
+ partners;
3319
+ audit;
3320
+ clock;
3321
+ compute(params) {
3322
+ const fiscalYear = typeof params.fiscalYear === "number" ? params.fiscalYear : null;
3323
+ const entries = fiscalYear === null ? this.journal.all() : this.journal.forFiscalYear(fiscalYear);
3324
+ const streams = {
3325
+ journal: entries.map((entry) => _JournalExportProjection.formatEntry(entry)),
3326
+ accounts: this.accounts.all().map((account) => withoutNulls(account.toJSON())),
3327
+ vouchers: this.vouchers.all().map((voucher) => withoutNulls(voucher.toJSON()))
3328
+ };
3329
+ if (this.partners.all().length > 0) {
3330
+ streams.partners = this.partners.all().map((partner) => partner.toJSON());
3331
+ }
3332
+ streams.auditLog = this.audit.all().map((record) => record.toJSON());
3333
+ const contentHashes = {};
3334
+ for (const [name, rows] of Object.entries(streams)) {
3335
+ const lines = rows.map((row) => canonicalJson(row));
3336
+ contentHashes[name] = createHash("sha256").update(lines.join("\n")).digest("hex");
3337
+ }
3338
+ const allFinalized = entries.every((entry) => entry.isFinalized());
3339
+ return {
3340
+ manifest: {
3341
+ formatVersion: FORMAT_VERSION,
3342
+ tenantId: this.tenantId.value,
3343
+ tenantName: this.tenantName,
3344
+ baseCurrency: this.baseCurrency.code,
3345
+ exportedAt: this.clock.now().toISOString(),
3346
+ hashAlgorithm: "sha256",
3347
+ streams: Object.keys(streams),
3348
+ contentHashes
3349
+ },
3350
+ fieldCatalogIncluded: true,
3351
+ fieldCatalog: this.fieldCatalog(),
3352
+ journal: { entryCount: entries.length, ordering: "sequenceNumber", allFinalized },
3353
+ data: streams
3354
+ };
3355
+ }
3356
+ static formatEntry(entry) {
3357
+ const data = entry.toJSON();
3358
+ const lines = Array.isArray(data.lines) ? data.lines : [];
3359
+ data.lines = lines.map((line) => {
3360
+ const source = line;
3361
+ const stripped = {};
3362
+ for (const field of LINE_FIELDS) stripped[field] = source[field];
3363
+ return stripped;
3364
+ });
3365
+ return data;
3366
+ }
3367
+ fieldCatalog() {
3368
+ return {
3369
+ journal: [
3370
+ { name: "id", type: "uuid", meaning: "Eindeutige Buchungs-ID (UUIDv7)" },
3371
+ { name: "sequenceNumber", type: "integer", meaning: "L\xFCckenlose Journalnummer je Gesch\xE4ftsjahr" },
3372
+ { name: "status", type: "string", meaning: "entered|finalized (Festschreibung)" },
3373
+ { name: "entryDate", type: "date", meaning: "Buchungsdatum (zonenlos)" },
3374
+ { name: "recordedAt", type: "timestamp", meaning: "Erfassungszeitpunkt" },
3375
+ { name: "periodRef", type: "object", meaning: "Gesch\xE4ftsjahr + Periode" },
3376
+ { name: "voucherId", type: "uuid", meaning: "Belegreferenz (Pflicht)" },
3377
+ { name: "text", type: "string", meaning: "Buchungstext" },
3378
+ { name: "lines", type: "array", meaning: "Positionen: Konto, Seite, Betrag, Dimensionen, Steuer-Tag" },
3379
+ { name: "reverses", type: "uuid|null", meaning: "R\xFCckverweis bei Storno (Generalumkehr)" },
3380
+ { name: "reversedBy", type: "uuid|null", meaning: "Verweis auf die Stornobuchung" }
3381
+ ],
3382
+ accounts: [
3383
+ { name: "number", type: "string", meaning: "Kontonummer (f\xFChrende Nullen signifikant)" },
3384
+ { name: "name", type: "string", meaning: "Kontobezeichnung" },
3385
+ { name: "type", type: "string", meaning: "asset|liability|equity|expense|revenue" },
3386
+ { name: "subtype", type: "string|null", meaning: "Kanonischer Subtyp (bank, ar, ap, \u2026)" }
3387
+ ],
3388
+ vouchers: [
3389
+ { name: "voucherNumber", type: "string", meaning: "Belegnummer" },
3390
+ { name: "voucherDate", type: "date", meaning: "Belegdatum" }
3391
+ ],
3392
+ auditLog: [
3393
+ { name: "at", type: "timestamp", meaning: "\xC4nderungszeitpunkt" },
3394
+ { name: "actor", type: "string", meaning: "Audit-Identit\xE4t" },
3395
+ { name: "action", type: "string", meaning: "created|corrected|finalized|locked|\u2026" },
3396
+ { name: "changes", type: "object", meaning: "Vorher/Nachher-Diff der ge\xE4nderten Felder" }
3397
+ ]
3398
+ };
3399
+ }
3400
+ };
3401
+
3402
+ // src/projection/datev-export.ts
3403
+ var TAX_SUBTYPES = /* @__PURE__ */ new Set(["tax_in", "tax_out"]);
3404
+ function pad2(value) {
3405
+ return String(value).padStart(2, "0");
3406
+ }
3407
+ function tagCode(line) {
3408
+ const code = line.taxTag?.code;
3409
+ return typeof code === "string" ? code : null;
3410
+ }
3411
+ var DatevExportProjection = class {
3412
+ constructor(journal, accounts, vouchers, partners, registry) {
3413
+ this.journal = journal;
3414
+ this.accounts = accounts;
3415
+ this.vouchers = vouchers;
3416
+ this.partners = partners;
3417
+ this.registry = registry;
3418
+ }
3419
+ journal;
3420
+ accounts;
3421
+ vouchers;
3422
+ partners;
3423
+ registry;
3424
+ compute(params) {
3425
+ const kind = typeof params.kind === "string" ? params.kind : "entries";
3426
+ const rows = kind === "accounts" ? this.accountRows() : kind === "partners" ? this.partnerRows() : this.entryRows(params);
3427
+ return { kind, rows, rowCount: rows.length };
3428
+ }
3429
+ entryRows(params) {
3430
+ const fiscalYear = typeof params.fiscalYear === "number" ? params.fiscalYear : null;
3431
+ const fromPeriod = typeof params.fromPeriod === "number" ? params.fromPeriod : 1;
3432
+ const throughPeriod = typeof params.throughPeriod === "number" ? params.throughPeriod : Number.MAX_SAFE_INTEGER;
3433
+ const entries = fiscalYear === null ? this.journal.all() : this.journal.forFiscalYear(fiscalYear);
3434
+ const rows = [];
3435
+ for (const entry of entries) {
3436
+ const period = entry.periodRef.period;
3437
+ if (period < fromPeriod || period > throughPeriod) continue;
3438
+ rows.push(...this.splitEntry(entry));
3439
+ }
3440
+ return rows;
3441
+ }
3442
+ splitEntry(entry) {
3443
+ let lead = null;
3444
+ const contraLines = [];
3445
+ const taxLines = [];
3446
+ for (const line of entry.lines()) {
3447
+ const account = this.accounts.byId(line.accountId);
3448
+ const isTaxLine = account?.subtype != null && TAX_SUBTYPES.has(account.subtype) && line.taxTag !== null;
3449
+ if (isTaxLine) {
3450
+ taxLines.push(line);
3451
+ continue;
3452
+ }
3453
+ if (lead === null && line.taxTag === null) {
3454
+ lead = line;
3455
+ continue;
3456
+ }
3457
+ contraLines.push(line);
3458
+ }
3459
+ if (lead === null || contraLines.length === 0) return [];
3460
+ const voucher = this.vouchers.byId(entry.voucherId);
3461
+ const rows = [];
3462
+ for (const contra of contraLines) {
3463
+ let gross = contra.money;
3464
+ let buKey = null;
3465
+ const contraCode = tagCode(contra);
3466
+ if (contraCode !== null) {
3467
+ buKey = this.registry.datevBuFor(contraCode);
3468
+ for (const taxLine of taxLines) {
3469
+ if (tagCode(taxLine) === contraCode) gross = gross.add(taxLine.money);
3470
+ }
3471
+ }
3472
+ rows.push({
3473
+ amount: gross.abs().amountAsString(),
3474
+ debitCredit: lead.side === "debit" ? "S" : "H",
3475
+ account: lead.account.value,
3476
+ contraAccount: contra.account.value,
3477
+ buKey,
3478
+ documentField1: voucher === null ? "" : voucher.voucherNumber,
3479
+ date: `${pad2(entry.entryDate.month())}${pad2(Number(entry.entryDate.iso.slice(8, 10)))}`,
3480
+ text: entry.text(),
3481
+ finalized: entry.isFinalized()
3482
+ });
3483
+ }
3484
+ return rows;
3485
+ }
3486
+ accountRows() {
3487
+ return this.accounts.all().map((account) => ({
3488
+ number: account.number.value,
3489
+ name: account.name,
3490
+ type: account.type
3491
+ }));
3492
+ }
3493
+ partnerRows() {
3494
+ return this.partners.all().map((partner) => partner.toJSON());
3495
+ }
3496
+ };
3497
+
3498
+ // src/mapping/mapping-registry.ts
3499
+ function isRecord5(value) {
3500
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3501
+ }
3502
+ var MappingRegistry = class _MappingRegistry {
3503
+ byIdMap = /* @__PURE__ */ new Map();
3504
+ static empty() {
3505
+ return new _MappingRegistry();
3506
+ }
3507
+ static fromRuleModules(raw) {
3508
+ const registry = new _MappingRegistry();
3509
+ for (const mappingData of raw) {
3510
+ if (isRecord5(mappingData)) registry.add(Mapping.fromData(mappingData));
3511
+ }
3512
+ return registry;
3513
+ }
3514
+ add(mapping) {
3515
+ this.byIdMap.set(mapping.id, mapping);
3516
+ }
3517
+ byId(id) {
3518
+ return this.byIdMap.get(id) ?? null;
3519
+ }
3520
+ };
3521
+
3522
+ // src/mapping/mapping-importer.ts
3523
+ function isRecord6(value) {
3524
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3525
+ }
3526
+ var MappingImporter = class {
3527
+ constructor(accounts, registry) {
3528
+ this.accounts = accounts;
3529
+ this.registry = registry;
3530
+ }
3531
+ accounts;
3532
+ registry;
3533
+ import(input) {
3534
+ const data = isRecord6(input.mapping) ? input.mapping : {};
3535
+ const mapping = Mapping.fromData(data);
3536
+ const gapWarnings = [];
3537
+ for (const account of this.relevantAccounts(mapping.kind)) {
3538
+ const matches = mapping.leaves.filter((leaf) => leafMatches(leaf, account.number.value)).map((leaf) => leaf.key);
3539
+ if (matches.length > 1) {
3540
+ throw new DomainError(
3541
+ "E_MAPPING_OVERLAP",
3542
+ `Konto ${account.number.value} f\xE4llt in mehrere Positionen: ${matches.join(", ")}`,
3543
+ { account: account.number.value, positions: matches }
3544
+ );
3545
+ }
3546
+ if (matches.length === 0) {
3547
+ gapWarnings.push({ account: account.number.value, assignedTo: "_unassigned" });
3548
+ }
3549
+ }
3550
+ this.registry.add(mapping);
3551
+ return { imported: true, id: mapping.id, kind: mapping.kind, gapWarnings };
3552
+ }
3553
+ relevantAccounts(kind) {
3554
+ return this.accounts.all().filter((account) => {
3555
+ if (kind === "balance-sheet") return isBalanceCarrying(account.type);
3556
+ if (kind === "income-statement") return !isBalanceCarrying(account.type);
3557
+ return false;
3558
+ });
3559
+ }
3560
+ };
3561
+
3562
+ // src/tax/tax-code.ts
3563
+ var TaxCode = class {
3564
+ constructor(code, versions, datevBu = null) {
3565
+ this.code = code;
3566
+ this.versions = versions;
3567
+ this.datevBu = datevBu;
3568
+ }
3569
+ code;
3570
+ versions;
3571
+ datevBu;
3572
+ versionFor(date) {
3573
+ for (const version of this.versions) {
3574
+ if (version.coversDate(date)) return version;
3575
+ }
3576
+ throw new DomainError(
3577
+ "E_TAXCODE_NO_VALID_VERSION",
3578
+ `Steuerschl\xFCssel ${this.code} hat keine zum ${date.iso} g\xFCltige Regelversion`,
3579
+ { code: this.code, date: date.iso }
3580
+ );
3581
+ }
3582
+ };
3583
+
3584
+ // src/tax/tax-code-version.ts
3585
+ var TaxCodeVersion = class {
3586
+ constructor(validFrom, validTo, rate, taxAccount, reportingKey, mechanism = "standard", inputTaxAccount = null, inputReportingKey = null, baseReportingKey = null) {
3587
+ this.validFrom = validFrom;
3588
+ this.validTo = validTo;
3589
+ this.rate = rate;
3590
+ this.taxAccount = taxAccount;
3591
+ this.reportingKey = reportingKey;
3592
+ this.mechanism = mechanism;
3593
+ this.inputTaxAccount = inputTaxAccount;
3594
+ this.inputReportingKey = inputReportingKey;
3595
+ this.baseReportingKey = baseReportingKey;
3596
+ }
3597
+ validFrom;
3598
+ validTo;
3599
+ rate;
3600
+ taxAccount;
3601
+ reportingKey;
3602
+ mechanism;
3603
+ inputTaxAccount;
3604
+ inputReportingKey;
3605
+ baseReportingKey;
3606
+ coversDate(date) {
3607
+ if (date.isBefore(this.validFrom)) return false;
3608
+ return this.validTo === null || !date.isAfter(this.validTo);
3609
+ }
3610
+ };
3611
+
3612
+ // src/tax/tax-code-registry.ts
3613
+ function asString5(value) {
3614
+ return typeof value === "string" ? value : null;
3615
+ }
3616
+ var TaxCodeRegistry = class _TaxCodeRegistry {
3617
+ constructor(codes) {
3618
+ this.codes = codes;
3619
+ }
3620
+ codes;
3621
+ static empty() {
3622
+ return new _TaxCodeRegistry(/* @__PURE__ */ new Map());
3623
+ }
3624
+ static fromData(data) {
3625
+ const codes = /* @__PURE__ */ new Map();
3626
+ for (const codeData of data) {
3627
+ const code = asString5(codeData.code) ?? "";
3628
+ const rawVersions = Array.isArray(codeData.versions) ? codeData.versions : [];
3629
+ const versions = [];
3630
+ for (const versionData of rawVersions) {
3631
+ if (versionData === null || typeof versionData !== "object") continue;
3632
+ const v = versionData;
3633
+ versions.push(
3634
+ new TaxCodeVersion(
3635
+ CalendarDate.of(asString5(v.validFrom) ?? ""),
3636
+ typeof v.validTo === "string" ? CalendarDate.of(v.validTo) : null,
3637
+ asString5(v.rate) ?? "0",
3638
+ asString5(v.taxAccount) ?? "",
3639
+ asString5(v.reportingKey),
3640
+ asString5(v.mechanism) ?? "standard",
3641
+ asString5(v.inputTaxAccount),
3642
+ asString5(v.inputReportingKey),
3643
+ asString5(v.baseReportingKey)
3644
+ )
3645
+ );
3646
+ }
3647
+ codes.set(code, new TaxCode(code, versions, asString5(codeData.datevBu)));
3648
+ }
3649
+ return new _TaxCodeRegistry(codes);
3650
+ }
3651
+ allVersions() {
3652
+ const versions = [];
3653
+ for (const code of this.codes.values()) versions.push(...code.versions);
3654
+ return versions;
3655
+ }
3656
+ datevBuFor(code) {
3657
+ return this.codes.get(code)?.datevBu ?? null;
3658
+ }
3659
+ get(code) {
3660
+ const found = this.codes.get(code);
3661
+ if (found === void 0) {
3662
+ throw new DomainError("E_TAXCODE_UNKNOWN", `Steuerschl\xFCssel "${code}" ist nicht definiert`, { code });
3663
+ }
3664
+ return found;
3665
+ }
3666
+ versionFor(code, date) {
3667
+ return this.get(code).versionFor(date);
3668
+ }
3669
+ };
3670
+
3671
+ // src/tax/tax-profile.ts
3672
+ var TaxProfile = class _TaxProfile {
3673
+ constructor(method, smallBusiness, period) {
3674
+ this.method = method;
3675
+ this.smallBusiness = smallBusiness;
3676
+ this.period = period;
3677
+ }
3678
+ method;
3679
+ smallBusiness;
3680
+ period;
3681
+ static fromData(data) {
3682
+ const method = data.taxationMethod === "cash" ? "cash" : "accrual";
3683
+ const period = data.vatPeriod === "monthly" ? "monthly" : "quarterly";
3684
+ const segments = [];
3685
+ const smallBusiness = data.smallBusiness ?? false;
3686
+ if (typeof smallBusiness === "boolean") {
3687
+ if (smallBusiness) segments.push({ validFrom: CalendarDate.of("0001-01-01"), value: true });
3688
+ } else if (Array.isArray(smallBusiness)) {
3689
+ for (const segment of smallBusiness) {
3690
+ if (segment === null || typeof segment !== "object") continue;
3691
+ const s = segment;
3692
+ if (typeof s.validFrom !== "string") continue;
3693
+ segments.push({ validFrom: CalendarDate.of(s.validFrom), value: s.value === true });
3694
+ }
3695
+ }
3696
+ return new _TaxProfile(method, _TaxProfile.sorted(segments), period);
3697
+ }
3698
+ static default() {
3699
+ return new _TaxProfile("accrual", [], "quarterly");
3700
+ }
3701
+ taxationMethod() {
3702
+ return this.method;
3703
+ }
3704
+ isCashBasis() {
3705
+ return this.method === "cash";
3706
+ }
3707
+ vatPeriod() {
3708
+ return this.period;
3709
+ }
3710
+ smallBusinessAt(date) {
3711
+ let value = false;
3712
+ for (const segment of this.smallBusiness) {
3713
+ if (segment.validFrom.isAfter(date)) break;
3714
+ value = segment.value;
3715
+ }
3716
+ return value;
3717
+ }
3718
+ setSmallBusiness(validFrom, value) {
3719
+ const segments = this.smallBusiness.filter((segment) => !segment.validFrom.equals(validFrom));
3720
+ segments.push({ validFrom, value });
3721
+ this.smallBusiness = _TaxProfile.sorted(segments);
3722
+ }
3723
+ static sorted(segments) {
3724
+ return [...segments].sort((a, b) => a.validFrom.compareTo(b.validFrom));
3725
+ }
3726
+ toJSON() {
3727
+ return {
3728
+ taxationMethod: this.method,
3729
+ vatPeriod: this.period,
3730
+ smallBusiness: this.smallBusiness.map((segment) => ({
3731
+ validFrom: segment.validFrom.iso,
3732
+ value: segment.value
3733
+ }))
3734
+ };
3735
+ }
3736
+ };
3737
+ function isRecord7(value) {
3738
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3739
+ }
3740
+ function asString6(value) {
3741
+ return typeof value === "string" ? value : null;
3742
+ }
3743
+ var TaxService = class {
3744
+ constructor(baseCurrency, registry, profileValue, journal) {
3745
+ this.baseCurrency = baseCurrency;
3746
+ this.registry = registry;
3747
+ this.profileValue = profileValue;
3748
+ this.journal = journal;
3749
+ }
3750
+ baseCurrency;
3751
+ registry;
3752
+ profileValue;
3753
+ journal;
3754
+ profile() {
3755
+ return this.profileValue;
3756
+ }
3757
+ registryHandle() {
3758
+ return this.registry;
3759
+ }
3760
+ expand(input) {
3761
+ const date = typeof input.serviceDate === "string" ? this.parseDate(input.serviceDate) : this.parseDate(input.date);
3762
+ const direction = input.direction === "input" ? "input" : "output";
3763
+ const defaultCode = asString6(input.taxCode);
3764
+ const rawLines = Array.isArray(input.netLines) ? input.netLines : [];
3765
+ if (rawLines.length === 0) {
3766
+ throw new DomainError("E_ENTRY_TOO_FEW_LINES", "expandTax ohne Netto-Positionen");
3767
+ }
3768
+ const netLines = rawLines.map((rawLine) => {
3769
+ if (!isRecord7(rawLine)) {
3770
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", "Netto-Position ist keine Struktur");
3771
+ }
3772
+ const code = asString6(rawLine.taxCode) ?? defaultCode;
3773
+ if (code === null) {
3774
+ throw new DomainError("E_TAXCODE_UNKNOWN", "Position ohne Steuerschl\xFCssel (kein Default gesetzt)");
3775
+ }
3776
+ return { account: asString6(rawLine.account) ?? "", money: this.parseMoney(rawLine.money), code };
3777
+ });
3778
+ for (const line of netLines) this.registry.get(line.code);
3779
+ const versions = /* @__PURE__ */ new Map();
3780
+ const bases = /* @__PURE__ */ new Map();
3781
+ for (const line of netLines) {
3782
+ if (!versions.has(line.code)) versions.set(line.code, this.registry.versionFor(line.code, date));
3783
+ bases.set(line.code, (bases.get(line.code) ?? Money.zero(this.baseCurrency)).add(line.money));
3784
+ }
3785
+ let netTotal = Money.zero(this.baseCurrency);
3786
+ for (const line of netLines) netTotal = netTotal.add(line.money);
3787
+ const sideFor = direction === "output" ? "credit" : "debit";
3788
+ if (this.profileValue.smallBusinessAt(date)) {
3789
+ return {
3790
+ netLines: netLines.map((line) => ({
3791
+ account: line.account,
3792
+ side: sideFor,
3793
+ money: line.money.toJSON(),
3794
+ taxTag: null
3795
+ })),
3796
+ taxLines: [],
3797
+ grossTotal: netTotal.toJSON()
3798
+ };
3799
+ }
3800
+ const codes = [...bases.keys()].sort((a, b) => {
3801
+ const aa = versions.get(a).taxAccount;
3802
+ const bb = versions.get(b).taxAccount;
3803
+ return aa < bb ? -1 : aa > bb ? 1 : 0;
3804
+ });
3805
+ const taxLines = [];
3806
+ let grossTotal = netTotal;
3807
+ const baseTags = /* @__PURE__ */ new Map();
3808
+ for (const code of codes) {
3809
+ const version = versions.get(code);
3810
+ const base = bases.get(code);
3811
+ const tax = Money.fromCalculation(
3812
+ new Big(base.amountAsString()).times(version.rate).div(100),
3813
+ this.baseCurrency
3814
+ );
3815
+ if (version.mechanism === "intra_community_supply") {
3816
+ baseTags.set(code, this.tag(code, version, version.reportingKey, base));
3817
+ continue;
3818
+ }
3819
+ if (version.mechanism === "reverse_charge") {
3820
+ taxLines.push({
3821
+ account: version.taxAccount,
3822
+ side: "credit",
3823
+ money: tax.toJSON(),
3824
+ taxTag: this.tag(code, version, version.reportingKey, base)
3825
+ });
3826
+ taxLines.push({
3827
+ account: version.inputTaxAccount ?? version.taxAccount,
3828
+ side: "debit",
3829
+ money: tax.toJSON(),
3830
+ taxTag: this.tag(code, version, version.inputReportingKey, base)
3831
+ });
3832
+ baseTags.set(code, this.tag(code, version, version.baseReportingKey ?? version.reportingKey, base));
3833
+ } else {
3834
+ taxLines.push({
3835
+ account: version.taxAccount,
3836
+ side: sideFor,
3837
+ money: tax.toJSON(),
3838
+ taxTag: this.tag(code, version, version.reportingKey, base)
3839
+ });
3840
+ baseTags.set(code, this.tag(code, version, version.reportingKey, base));
3841
+ grossTotal = grossTotal.add(tax);
3842
+ }
3843
+ }
3844
+ return {
3845
+ netLines: netLines.map((line) => ({
3846
+ account: line.account,
3847
+ side: sideFor,
3848
+ money: line.money.toJSON(),
3849
+ taxTag: baseTags.get(line.code) ?? null
3850
+ })),
3851
+ taxLines,
3852
+ grossTotal: grossTotal.toJSON()
3853
+ };
3854
+ }
3855
+ setProfile(input) {
3856
+ const smallBusiness = input.smallBusiness;
3857
+ if (!isRecord7(smallBusiness) || typeof smallBusiness.validFrom !== "string") {
3858
+ throw new DomainError("E_PROFILE_RETROACTIVE_CONFLICT", "setTaxProfile braucht smallBusiness.validFrom");
3859
+ }
3860
+ const validFrom = this.parseDate(smallBusiness.validFrom);
3861
+ for (const entry of this.journal.all()) {
3862
+ if (entry.isFinalized() && !entry.entryDate.isBefore(validFrom)) {
3863
+ throw new DomainError(
3864
+ "E_PROFILE_RETROACTIVE_CONFLICT",
3865
+ `Zeitraum ab ${validFrom.iso} enth\xE4lt festgeschriebene Buchungen (z. B. Nr. ${entry.sequenceNumber})`,
3866
+ { validFrom: validFrom.iso, sequenceNumber: entry.sequenceNumber }
3867
+ );
3868
+ }
3869
+ }
3870
+ this.profileValue.setSmallBusiness(validFrom, smallBusiness.value === true);
3871
+ return this.profileValue;
3872
+ }
3873
+ tag(code, version, reportingKey, base) {
3874
+ return {
3875
+ code,
3876
+ appliedVersion: version.validFrom.iso,
3877
+ reportingKey,
3878
+ baseMoney: base.toJSON()
3879
+ };
3880
+ }
3881
+ parseDate(date) {
3882
+ try {
3883
+ return CalendarDate.of(typeof date === "string" ? date : "");
3884
+ } catch (error) {
3885
+ if (error instanceof InvalidValue) {
3886
+ throw new DomainError("E_TAXCODE_NO_VALID_VERSION", "Belegdatum fehlt oder ung\xFCltig");
3887
+ }
3888
+ throw error;
3889
+ }
3890
+ }
3891
+ parseMoney(raw) {
3892
+ const amount = isRecord7(raw) ? asString6(raw.amount) : null;
3893
+ if (amount === null) {
3894
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", "Netto-Position ohne Betrag");
3895
+ }
3896
+ try {
3897
+ return Money.of(amount, this.baseCurrency);
3898
+ } catch (error) {
3899
+ if (error instanceof InvalidValue) {
3900
+ throw new DomainError("E_ENTRY_INVALID_AMOUNT", `Ung\xFCltiger Betrag "${amount}"`);
3901
+ }
3902
+ throw error;
3903
+ }
3904
+ }
3905
+ };
3906
+
3907
+ // src/composition/tenant.ts
3908
+ var Tenant = class _Tenant {
3909
+ constructor(id, name, baseCurrency, accounts, fiscalYears, vouchers, journal, openItems, assets, partners, audit, ledger, tax, assetService, costing, partnerService, mappings, clock, ids) {
3910
+ this.id = id;
3911
+ this.name = name;
3912
+ this.baseCurrency = baseCurrency;
3913
+ this.accounts = accounts;
3914
+ this.fiscalYears = fiscalYears;
3915
+ this.vouchers = vouchers;
3916
+ this.journal = journal;
3917
+ this.openItems = openItems;
3918
+ this.assets = assets;
3919
+ this.partners = partners;
3920
+ this.audit = audit;
3921
+ this.ledger = ledger;
3922
+ this.tax = tax;
3923
+ this.assetService = assetService;
3924
+ this.costing = costing;
3925
+ this.partnerService = partnerService;
3926
+ this.mappings = mappings;
3927
+ this.clock = clock;
3928
+ this.ids = ids;
3929
+ }
3930
+ id;
3931
+ name;
3932
+ baseCurrency;
3933
+ accounts;
3934
+ fiscalYears;
3935
+ vouchers;
3936
+ journal;
3937
+ openItems;
3938
+ assets;
3939
+ partners;
3940
+ audit;
3941
+ ledger;
3942
+ tax;
3943
+ assetService;
3944
+ costing;
3945
+ partnerService;
3946
+ mappings;
3947
+ clock;
3948
+ ids;
3949
+ static inMemory(name, baseCurrency, clock = new SystemClock(), ids, dimensions = DimensionRegistry.empty(), taxCodes = TaxCodeRegistry.empty(), taxProfile = TaxProfile.default(), mappings = MappingRegistry.empty()) {
3950
+ const idGen = ids ?? new UuidV7IdGenerator(clock);
3951
+ const accounts = new InMemoryAccountRepository();
3952
+ const fiscalYears = new InMemoryFiscalYearRepository();
3953
+ const vouchers = new InMemoryVoucherRepository();
3954
+ const journal = new InMemoryJournalRepository();
3955
+ const openItems = new InMemoryOpenItemRepository();
3956
+ const assets = new InMemoryAssetRepository();
3957
+ const partners = new InMemoryPartnerRepository();
3958
+ const audit = new InMemoryAuditTrail();
3959
+ const ledger = new Ledger(
3960
+ baseCurrency,
3961
+ accounts,
3962
+ fiscalYears,
3963
+ vouchers,
3964
+ journal,
3965
+ openItems,
3966
+ audit,
3967
+ dimensions,
3968
+ clock,
3969
+ idGen
3970
+ );
3971
+ const tax = new TaxService(baseCurrency, taxCodes, taxProfile, journal);
3972
+ const assetService = new AssetService(baseCurrency, assets, fiscalYears, vouchers, ledger, idGen);
3973
+ const costing = new CostingService(baseCurrency, accounts, journal, idGen);
3974
+ const partnerService = new PartnerService(partners, audit, clock, idGen);
3975
+ return new _Tenant(
3976
+ idGen.next(),
3977
+ name,
3978
+ baseCurrency,
3979
+ accounts,
3980
+ fiscalYears,
3981
+ vouchers,
3982
+ journal,
3983
+ openItems,
3984
+ assets,
3985
+ partners,
3986
+ audit,
3987
+ ledger,
3988
+ tax,
3989
+ assetService,
3990
+ costing,
3991
+ partnerService,
3992
+ mappings,
3993
+ clock,
3994
+ idGen
3995
+ );
3996
+ }
3997
+ };
3998
+
3999
+ // src/composition/post-voucher-service.ts
4000
+ function isRecord8(value) {
4001
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4002
+ }
4003
+ function asString7(value) {
4004
+ return typeof value === "string" ? value : null;
4005
+ }
4006
+ var PostVoucherService = class {
4007
+ constructor(tenant) {
4008
+ this.tenant = tenant;
4009
+ }
4010
+ tenant;
4011
+ post(input) {
4012
+ const voucherData = isRecord8(input.voucher) ? input.voucher : {};
4013
+ const voucherNumber = asString7(voucherData.voucherNumber) ?? "";
4014
+ let voucherDate;
4015
+ try {
4016
+ voucherDate = CalendarDate.of(asString7(voucherData.voucherDate) ?? "");
4017
+ } catch (error) {
4018
+ if (error instanceof InvalidValue) {
4019
+ throw new DomainError("E_ENTRY_NO_VOUCHER", "postVoucher braucht voucher.voucherDate");
4020
+ }
4021
+ throw error;
4022
+ }
4023
+ let partnerId = null;
4024
+ if (voucherData.partnerId !== void 0 && voucherData.partnerId !== null) {
4025
+ partnerId = this.tenant.partnerService.require(voucherData.partnerId).id;
4026
+ }
4027
+ const date = (value) => typeof value === "string" ? CalendarDate.of(value) : null;
4028
+ const servicePeriod = isRecord8(voucherData.servicePeriod) ? voucherData.servicePeriod : {};
4029
+ const voucher = new Voucher({
4030
+ id: this.tenant.ids.next(),
4031
+ voucherNumber,
4032
+ voucherDate,
4033
+ due: date(voucherData.due),
4034
+ recurring: voucherData.recurring === true,
4035
+ economicYear: typeof voucherData.economicYear === "number" ? voucherData.economicYear : null,
4036
+ serviceDate: date(voucherData.serviceDate),
4037
+ servicePeriodFrom: date(servicePeriod.from),
4038
+ servicePeriodTo: date(servicePeriod.to),
4039
+ kind: asString7(voucherData.kind),
4040
+ partnerId,
4041
+ issuer: asString7(voucherData.issuer)
4042
+ });
4043
+ this.tenant.vouchers.add(voucher);
4044
+ const expansion = this.tenant.tax.expand({
4045
+ date: voucherDate.iso,
4046
+ serviceDate: voucher.taxDate().iso,
4047
+ taxCode: input.taxCode ?? null,
4048
+ direction: input.direction ?? "output",
4049
+ netLines: input.netLines ?? []
4050
+ });
4051
+ const direction = input.direction === "input" ? "input" : "output";
4052
+ const counterAccount = asString7(input.counterAccount) ?? "";
4053
+ const netLines = Array.isArray(expansion.netLines) ? expansion.netLines : [];
4054
+ const taxLines = Array.isArray(expansion.taxLines) ? expansion.taxLines : [];
4055
+ const lines = [
4056
+ {
4057
+ account: counterAccount,
4058
+ side: direction === "output" ? "debit" : "credit",
4059
+ money: expansion.grossTotal
4060
+ },
4061
+ ...netLines,
4062
+ ...taxLines
4063
+ ];
4064
+ const result = this.tenant.ledger.post({
4065
+ actor: input.actor ?? null,
4066
+ entryDate: input.entryDate ?? voucherDate.iso,
4067
+ voucherId: voucher.id.value,
4068
+ text: input.text ?? "",
4069
+ lines
4070
+ });
4071
+ return {
4072
+ entry: JSON.parse(JSON.stringify(result.entry)),
4073
+ openItemsCreated: result.openItemsCreated.map((item) => JSON.parse(JSON.stringify(item))),
4074
+ grossTotal: expansion.grossTotal,
4075
+ taxLines: expansion.taxLines,
4076
+ voucherId: voucher.id.value
4077
+ };
4078
+ }
4079
+ };
4080
+
4081
+ // src/composition/tenant-operations.ts
4082
+ function serialize(value) {
4083
+ return JSON.parse(JSON.stringify(value));
4084
+ }
4085
+ var TenantOperations = class {
4086
+ constructor(tenant) {
4087
+ this.tenant = tenant;
4088
+ }
4089
+ tenant;
4090
+ execute(op, input) {
4091
+ const ledger = this.tenant.ledger;
4092
+ switch (op) {
4093
+ case "expandTax":
4094
+ return this.tenant.tax.expand(input);
4095
+ case "setTaxProfile":
4096
+ return serialize(this.tenant.tax.setProfile(input));
4097
+ case "postVoucher":
4098
+ return new PostVoucherService(this.tenant).post(input);
4099
+ case "post": {
4100
+ const result = ledger.post(input);
4101
+ return {
4102
+ ...serialize(result.entry),
4103
+ openItemsCreated: result.openItemsCreated.map((item) => serialize(item))
4104
+ };
4105
+ }
4106
+ case "correct":
4107
+ return serialize(ledger.correct(input));
4108
+ case "settle":
4109
+ return { openItems: ledger.settle(input).map((item) => serialize(item)) };
4110
+ case "finalize":
4111
+ return { finalizedCount: ledger.finalize(input) };
4112
+ case "reverse":
4113
+ return serialize(ledger.reverse(input));
4114
+ case "closePeriod":
4115
+ return ledger.closePeriod(input);
4116
+ case "reopenPeriod":
4117
+ return ledger.reopenPeriod(input);
4118
+ case "closeFiscalYear":
4119
+ return { fiscalYear: ledger.closeFiscalYear(input).year, status: "closed" };
4120
+ case "createAccount":
4121
+ return serialize(ledger.createAccount(input));
4122
+ case "createFiscalYear": {
4123
+ const fiscalYear = ledger.createFiscalYear(input);
4124
+ return { year: fiscalYear.year, periodCount: fiscalYear.periods().length };
4125
+ }
4126
+ case "lockAccount":
4127
+ return serialize(ledger.lockAccount(input));
4128
+ case "importChartOfAccounts":
4129
+ return { importedCount: ledger.importChartOfAccounts(input) };
4130
+ case "importMapping":
4131
+ return new MappingImporter(this.tenant.accounts, this.tenant.mappings).import(input);
4132
+ case "createPartner":
4133
+ return serialize(this.tenant.partnerService.create(input));
4134
+ case "updatePartner":
4135
+ return serialize(this.tenant.partnerService.update(input));
4136
+ case "acquireAsset":
4137
+ return this.tenant.assetService.acquire(input);
4138
+ case "disposeAsset":
4139
+ return this.tenant.assetService.dispose(input);
4140
+ case "runDepreciation":
4141
+ return this.tenant.assetService.runDepreciation(input);
4142
+ case "setAllocationScheme":
4143
+ return this.tenant.costing.setAllocationScheme(input);
4144
+ case "runCosting": {
4145
+ const run = this.tenant.costing.run(input);
4146
+ return { runId: run.id.value, status: run.status(), version: run.version };
4147
+ }
4148
+ case "releaseCosting": {
4149
+ const released = this.tenant.costing.release(input);
4150
+ return { runId: released.id.value, status: released.status() };
4151
+ }
4152
+ default:
4153
+ throw new DomainError("E_NOT_IMPLEMENTED", `Operation "${op}" ist nicht definiert`);
4154
+ }
4155
+ }
4156
+ project(name, params) {
4157
+ const tenant = this.tenant;
4158
+ switch (name) {
4159
+ case "trialBalance":
4160
+ return new TrialBalanceProjection(tenant.baseCurrency, tenant.accounts, tenant.journal).compute(params);
4161
+ case "openItems":
4162
+ return new OpenItemsProjection(tenant.openItems, tenant.vouchers, tenant.journal).compute(params);
4163
+ case "accountSheet":
4164
+ return new AccountSheetProjection(tenant.baseCurrency, tenant.accounts, tenant.journal).compute(params);
4165
+ case "auditLog":
4166
+ return new AuditLogProjection(tenant.audit).compute(params);
4167
+ case "assetRegister":
4168
+ return new AssetRegisterProjection(tenant.assets).compute(params);
4169
+ case "costAllocationSheet":
4170
+ return tenant.costing.costAllocationSheet(params);
4171
+ case "ecSalesList":
4172
+ return new EcSalesListProjection(
4173
+ tenant.baseCurrency,
4174
+ tenant.journal,
4175
+ tenant.vouchers,
4176
+ tenant.partners,
4177
+ tenant.tax.registryHandle()
4178
+ ).compute(params);
4179
+ case "incomeStatement":
4180
+ return new IncomeStatementProjection(
4181
+ tenant.baseCurrency,
4182
+ tenant.accounts,
4183
+ tenant.journal,
4184
+ tenant.mappings
4185
+ ).compute(params);
4186
+ case "balanceSheet":
4187
+ return new BalanceSheetProjection(
4188
+ tenant.baseCurrency,
4189
+ tenant.accounts,
4190
+ tenant.journal,
4191
+ tenant.mappings
4192
+ ).compute(params);
4193
+ case "vatReturn":
4194
+ return new VatReturnProjection(
4195
+ tenant.baseCurrency,
4196
+ tenant.journal,
4197
+ tenant.openItems,
4198
+ tenant.vouchers,
4199
+ tenant.accounts,
4200
+ tenant.tax.registryHandle(),
4201
+ tenant.tax.profile()
4202
+ ).compute(params);
4203
+ case "cashBasisReport":
4204
+ return new CashBasisProjection(
4205
+ tenant.baseCurrency,
4206
+ tenant.accounts,
4207
+ tenant.journal,
4208
+ tenant.openItems,
4209
+ tenant.vouchers,
4210
+ tenant.fiscalYears,
4211
+ tenant.mappings
4212
+ ).compute(params);
4213
+ case "journalExport":
4214
+ return new JournalExportProjection(
4215
+ tenant.id,
4216
+ tenant.name,
4217
+ tenant.baseCurrency,
4218
+ tenant.journal,
4219
+ tenant.accounts,
4220
+ tenant.vouchers,
4221
+ tenant.partners,
4222
+ tenant.audit,
4223
+ tenant.clock
4224
+ ).compute(params);
4225
+ case "datevExport":
4226
+ return new DatevExportProjection(
4227
+ tenant.journal,
4228
+ tenant.accounts,
4229
+ tenant.vouchers,
4230
+ tenant.partners,
4231
+ tenant.tax.registryHandle()
4232
+ ).compute(params);
4233
+ default:
4234
+ throw new DomainError("E_NOT_IMPLEMENTED", `Projektion "${name}" ist nicht definiert`);
4235
+ }
4236
+ }
4237
+ };
4238
+
4239
+ // src/composition/tenant-factory.ts
4240
+ function isRecord9(value) {
4241
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4242
+ }
4243
+ function asString8(value) {
4244
+ return typeof value === "string" ? value : null;
4245
+ }
4246
+ var TenantFactory = class {
4247
+ constructor(ruleModules, clock, ids) {
4248
+ this.ruleModules = ruleModules;
4249
+ this.clock = clock;
4250
+ this.ids = ids;
4251
+ }
4252
+ ruleModules;
4253
+ clock;
4254
+ ids;
4255
+ create(input) {
4256
+ const profileId = asString8(input.profile) ?? "";
4257
+ const profile = this.findById("profiles", profileId);
4258
+ if (profile === null) {
4259
+ throw new DomainError("E_PROFILE_UNKNOWN", `Profil "${profileId}" ist nicht vorhanden`);
4260
+ }
4261
+ const coaId = asString8(profile.chartOfAccounts) ?? "";
4262
+ const coa = this.findById("chartsOfAccounts", coaId);
4263
+ if (coa === null) {
4264
+ throw new DomainError("E_PROFILE_UNKNOWN", `Kontenrahmen "${coaId}" des Profils fehlt`);
4265
+ }
4266
+ const wantedCodes = Array.isArray(profile.taxCodes) ? profile.taxCodes : [];
4267
+ const allTaxCodes = Array.isArray(this.ruleModules.taxCodes) ? this.ruleModules.taxCodes : [];
4268
+ const taxCodeData = allTaxCodes.filter(
4269
+ (code) => isRecord9(code) && wantedCodes.includes(code.code)
4270
+ );
4271
+ const defaults = isRecord9(profile.defaults) ? profile.defaults : {};
4272
+ const taxProfile = TaxProfile.fromData(defaults);
4273
+ const tenant = Tenant.inMemory(
4274
+ asString8(input.name) ?? "Tenant",
4275
+ Currency.of(asString8(input.baseCurrency) ?? "EUR"),
4276
+ this.clock,
4277
+ this.ids,
4278
+ void 0,
4279
+ TaxCodeRegistry.fromData(taxCodeData),
4280
+ taxProfile
4281
+ );
4282
+ let accountCount = 0;
4283
+ for (const accountData of Array.isArray(coa.accounts) ? coa.accounts : []) {
4284
+ if (!isRecord9(accountData)) continue;
4285
+ const type = accountData.type;
4286
+ if (!isAccountType(type)) continue;
4287
+ tenant.accounts.add(
4288
+ new Account(
4289
+ tenant.ids.next(),
4290
+ AccountNumber.of(asString8(accountData.number) ?? ""),
4291
+ asString8(accountData.name) ?? "",
4292
+ type,
4293
+ asString8(accountData.subtype),
4294
+ "active"
4295
+ )
4296
+ );
4297
+ accountCount++;
4298
+ }
4299
+ const year = typeof input.firstFiscalYear === "number" ? input.firstFiscalYear : 0;
4300
+ if (year > 0) {
4301
+ const y = String(year).padStart(4, "0");
4302
+ tenant.fiscalYears.add(
4303
+ FiscalYear.create(tenant.ids.next(), year, CalendarDate.of(`${y}-01-01`), CalendarDate.of(`${y}-12-31`))
4304
+ );
4305
+ }
4306
+ return {
4307
+ tenant,
4308
+ result: {
4309
+ id: tenant.id.value,
4310
+ name: tenant.name,
4311
+ profile: { id: profileId, version: asString8(profile.version) ?? "" },
4312
+ accountCount,
4313
+ taxationMethod: taxProfile.taxationMethod()
4314
+ }
4315
+ };
4316
+ }
4317
+ findById(module, id) {
4318
+ const list = Array.isArray(this.ruleModules[module]) ? this.ruleModules[module] : [];
4319
+ for (const candidate of list) {
4320
+ if (isRecord9(candidate) && candidate.id === id) return candidate;
4321
+ }
4322
+ return null;
4323
+ }
4324
+ };
4325
+
4326
+ export { Account, AccountNumber, AccountSheetProjection, Asset, AssetRegisterProjection, AssetService, AuditLogProjection, AuditRecord, BalanceSheetProjection, CalendarDate, CashBasisProjection, CostingRun, CostingService, Currency, CurrencyMismatch, DatevExportProjection, DeterministicIdGenerator, DimensionRegistry, DimensionValue, DomainError, EcSalesListProjection, EntryLine, FiscalYear, FixedClock, InMemoryAccountRepository, InMemoryAssetRepository, InMemoryAuditTrail, InMemoryFiscalYearRepository, InMemoryJournalRepository, InMemoryOpenItemRepository, InMemoryPartnerRepository, InMemoryVoucherRepository, IncomeStatementProjection, InvalidValue, JournalEntry, JournalExportProjection, Ledger, Mapping, MappingImporter, MappingRegistry, Money, OpenItem, OpenItemsProjection, Partner, PartnerService, Period, PeriodRef, PostResult, PostVoucherService, Settlement, SystemClock, TaxCode, TaxCodeRegistry, TaxCodeVersion, TaxProfile, TaxService, Tenant, TenantFactory, TenantOperations, TrialBalanceProjection, Uuid, UuidV7IdGenerator, VatReturnProjection, Voucher, canonicalJson, isAccountType, isBalanceCarrying, leafMatches, parseAssetRoute, parseOpenItemKind, parseSettlementDifferenceKind };
4327
+ //# sourceMappingURL=index.js.map
4328
+ //# sourceMappingURL=index.js.map