@fin.cx/skr 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * OPOS (Offene-Posten-Buchhaltung): open-item accounting over the v2 ledger.
3
+ *
4
+ * Open items are DERIVED from the hashed journal — no separate bookkeeping
5
+ * state that could drift. A line on a personal account (debtors 10000-69999,
6
+ * creditors 70000-99999) belongs to the open item keyed by
7
+ * (account, belegfeld1); the item is open while its signed sum is non-zero.
8
+ * Settling an item is ordinary posting (core.bookBankPayment + JournalPoster),
9
+ * so Festschreibung and the audit chain apply unchanged.
10
+ *
11
+ * Only dunning metadata lives outside the ledger (OposMeta collection) —
12
+ * process state, not bookkeeping.
13
+ */
14
+ import * as core from './core/index.js';
15
+ import { getDbSync } from './skr.database.js';
16
+ import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
17
+ export const OPOS_META_COLLECTION = 'OposMeta';
18
+ function kindOf(accountNumber, policy) {
19
+ if (core.isDebtorAccount(policy, accountNumber))
20
+ return 'debtor';
21
+ if (core.isCreditorAccount(policy, accountNumber))
22
+ return 'creditor';
23
+ return null;
24
+ }
25
+ async function loadPersonalLines(skrType, policy, accountNumber, asOf) {
26
+ const match = {
27
+ skrType,
28
+ schemaVersion: 2,
29
+ status: { $in: ['posted', 'finalized'] },
30
+ };
31
+ if (asOf)
32
+ match.date = { $lte: asOf };
33
+ if (accountNumber)
34
+ match['lines.accountNumber'] = accountNumber;
35
+ const docs = await getDbSync()
36
+ .mongoDb.collection(JOURNAL_ENTRY_COLLECTION)
37
+ .find(match)
38
+ .sort({ date: 1, sequenceNumber: 1 })
39
+ .toArray();
40
+ const rows = [];
41
+ for (const doc of docs) {
42
+ for (const line of (doc.lines ?? [])) {
43
+ const account = line.accountNumber;
44
+ if (!kindOf(account, policy))
45
+ continue;
46
+ if (accountNumber && account !== accountNumber)
47
+ continue;
48
+ rows.push({
49
+ entryId: doc.id,
50
+ accountNumber: account,
51
+ belegfeld1: line.belegfeld1 || doc.reference || '',
52
+ side: line.side,
53
+ amountCents: line.amountCents ?? 0,
54
+ date: doc.date,
55
+ });
56
+ }
57
+ }
58
+ return rows;
59
+ }
60
+ /**
61
+ * Derive open items. Debtors: claims are debits, settlements credits;
62
+ * creditors inverse. Items with openCents === 0 are settled and omitted
63
+ * unless includeSettled is set.
64
+ */
65
+ export async function listOpenItems(skrType, policy, options) {
66
+ const rows = await loadPersonalLines(skrType, policy, options?.accountNumber, options?.asOf);
67
+ const groups = new Map();
68
+ for (const row of rows) {
69
+ const kind = kindOf(row.accountNumber, policy);
70
+ if (options?.kind && kind !== options.kind)
71
+ continue;
72
+ const key = `${row.accountNumber}:${row.belegfeld1}`;
73
+ let item = groups.get(key);
74
+ if (!item) {
75
+ item = {
76
+ accountNumber: row.accountNumber,
77
+ kind,
78
+ reference: row.belegfeld1 ?? '',
79
+ invoiceCents: 0,
80
+ clearedCents: 0,
81
+ openCents: 0,
82
+ firstDate: row.date.toISOString().slice(0, 10),
83
+ lastDate: row.date.toISOString().slice(0, 10),
84
+ entryIds: [],
85
+ dunningLevel: 0,
86
+ };
87
+ groups.set(key, item);
88
+ }
89
+ const isClaim = (kind === 'debtor') === (row.side === 'debit');
90
+ if (isClaim)
91
+ item.invoiceCents += row.amountCents;
92
+ else
93
+ item.clearedCents += row.amountCents;
94
+ item.lastDate = row.date.toISOString().slice(0, 10);
95
+ if (!item.entryIds.includes(row.entryId))
96
+ item.entryIds.push(row.entryId);
97
+ }
98
+ const meta = await getDbSync().mongoDb.collection(OPOS_META_COLLECTION).find({ skrType }).toArray();
99
+ const metaByKey = new Map(meta.map((m) => [`${m.accountNumber}:${m.reference}`, m]));
100
+ const items = [];
101
+ for (const item of groups.values()) {
102
+ item.openCents = item.invoiceCents - item.clearedCents;
103
+ const extra = metaByKey.get(`${item.accountNumber}:${item.reference}`);
104
+ if (extra) {
105
+ item.dunningLevel = extra.dunningLevel ?? 0;
106
+ item.dueDate = extra.dueDate;
107
+ }
108
+ if (item.openCents !== 0 || options?.includeSettled)
109
+ items.push(item);
110
+ }
111
+ return items.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber) || a.firstDate.localeCompare(b.firstDate));
112
+ }
113
+ /**
114
+ * Aging list relative to asOf, bucketed by days since dueDate (falling back
115
+ * to the item's first posting date).
116
+ */
117
+ export async function getAging(skrType, policy, kind, asOf, bucketDays = [30, 60, 90]) {
118
+ const items = await listOpenItems(skrType, policy, { kind, asOf });
119
+ const buckets = [
120
+ ...bucketDays.map((days, index) => ({
121
+ label: index === 0 ? `0-${days}` : `${bucketDays[index - 1] + 1}-${days}`,
122
+ maxAgeDays: days,
123
+ items: [],
124
+ totalCents: 0,
125
+ })),
126
+ { label: `> ${bucketDays[bucketDays.length - 1]}`, maxAgeDays: null, items: [], totalCents: 0 },
127
+ ];
128
+ for (const item of items) {
129
+ const anchor = item.dueDate ?? item.firstDate;
130
+ const ageDays = Math.max(0, Math.floor((asOf.getTime() - new Date(anchor).getTime()) / 86_400_000));
131
+ const bucket = buckets.find((candidate) => candidate.maxAgeDays !== null && ageDays <= candidate.maxAgeDays) ??
132
+ buckets[buckets.length - 1];
133
+ bucket.items.push(item);
134
+ bucket.totalCents += item.openCents;
135
+ }
136
+ return buckets;
137
+ }
138
+ /**
139
+ * Subledger control totals: the aggregated personal-account balances that a
140
+ * balance sheet reports as Forderungen (B.II.1) / Verbindlichkeiten (C.4)
141
+ * aus Lieferungen und Leistungen. Control accounts themselves are blocked for
142
+ * posting, so these aggregates ARE the control balances.
143
+ */
144
+ export async function getControlTotals(skrType, policy, asOf) {
145
+ const rows = await loadPersonalLines(skrType, policy, undefined, asOf);
146
+ let debtorsCents = 0;
147
+ let creditorsCents = 0;
148
+ const debtorAccounts = new Set();
149
+ const creditorAccounts = new Set();
150
+ for (const row of rows) {
151
+ const signed = row.side === 'debit' ? row.amountCents : -row.amountCents;
152
+ if (kindOf(row.accountNumber, policy) === 'debtor') {
153
+ debtorsCents += signed;
154
+ debtorAccounts.add(row.accountNumber);
155
+ }
156
+ else {
157
+ creditorsCents -= signed; // creditors carry credit balances positive
158
+ creditorAccounts.add(row.accountNumber);
159
+ }
160
+ }
161
+ return {
162
+ debtorsCents,
163
+ creditorsCents,
164
+ debtorAccounts: debtorAccounts.size,
165
+ creditorAccounts: creditorAccounts.size,
166
+ };
167
+ }
168
+ /**
169
+ * Dunning metadata (process state, not bookkeeping — letters are out of
170
+ * scope; the level/due-date state machine is in).
171
+ */
172
+ export async function setOposMeta(params) {
173
+ const update = { updatedAt: new Date() };
174
+ if (params.dunningLevel !== undefined)
175
+ update.dunningLevel = params.dunningLevel;
176
+ if (params.dueDate !== undefined)
177
+ update.dueDate = params.dueDate;
178
+ await getDbSync()
179
+ .mongoDb.collection(OPOS_META_COLLECTION)
180
+ .updateOne({ skrType: params.skrType, accountNumber: params.accountNumber, reference: params.reference }, { $set: update }, { upsert: true });
181
+ }
182
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLm9wb3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9za3Iub3Bvcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxPQUFPLEtBQUssSUFBSSxNQUFNLGlCQUFpQixDQUFDO0FBQ3hDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUczRCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxVQUFVLENBQUM7QUFvQy9DLFNBQVMsTUFBTSxDQUFDLGFBQXFCLEVBQUUsTUFBMkI7SUFDaEUsSUFBSSxJQUFJLENBQUMsZUFBZSxDQUFDLE1BQU0sRUFBRSxhQUFhLENBQUM7UUFBRSxPQUFPLFFBQVEsQ0FBQztJQUNqRSxJQUFJLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsYUFBYSxDQUFDO1FBQUUsT0FBTyxVQUFVLENBQUM7SUFDckUsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBV0QsS0FBSyxVQUFVLGlCQUFpQixDQUM5QixPQUFpQixFQUNqQixNQUEyQixFQUMzQixhQUFzQixFQUN0QixJQUFXO0lBRVgsTUFBTSxLQUFLLEdBQTRCO1FBQ3JDLE9BQU87UUFDUCxhQUFhLEVBQUUsQ0FBQztRQUNoQixNQUFNLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLEVBQUU7S0FDekMsQ0FBQztJQUNGLElBQUksSUFBSTtRQUFFLEtBQUssQ0FBQyxJQUFJLEdBQUcsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDdEMsSUFBSSxhQUFhO1FBQUUsS0FBSyxDQUFDLHFCQUFxQixDQUFDLEdBQUcsYUFBYSxDQUFDO0lBRWhFLE1BQU0sSUFBSSxHQUFHLE1BQU0sU0FBUyxFQUFFO1NBQzNCLE9BQU8sQ0FBQyxVQUFVLENBQUMsd0JBQXdCLENBQUM7U0FDNUMsSUFBSSxDQUFDLEtBQUssQ0FBQztTQUNYLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSxDQUFDLEVBQUUsY0FBYyxFQUFFLENBQUMsRUFBRSxDQUFDO1NBQ3BDLE9BQU8sRUFBRSxDQUFDO0lBRWIsTUFBTSxJQUFJLEdBQWUsRUFBRSxDQUFDO0lBQzVCLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSSxFQUFFLENBQUM7UUFDdkIsS0FBSyxNQUFNLElBQUksSUFBSSxDQUFDLEdBQUcsQ0FBQyxLQUFLLElBQUksRUFBRSxDQUFtQyxFQUFFLENBQUM7WUFDdkUsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLGFBQXVCLENBQUM7WUFDN0MsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDO2dCQUFFLFNBQVM7WUFDdkMsSUFBSSxhQUFhLElBQUksT0FBTyxLQUFLLGFBQWE7Z0JBQUUsU0FBUztZQUN6RCxJQUFJLENBQUMsSUFBSSxDQUFDO2dCQUNSLE9BQU8sRUFBRSxHQUFHLENBQUMsRUFBWTtnQkFDekIsYUFBYSxFQUFFLE9BQU87Z0JBQ3RCLFVBQVUsRUFBRyxJQUFJLENBQUMsVUFBcUIsSUFBSyxHQUFHLENBQUMsU0FBb0IsSUFBSSxFQUFFO2dCQUMxRSxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQTBCO2dCQUNyQyxXQUFXLEVBQUcsSUFBSSxDQUFDLFdBQXNCLElBQUksQ0FBQztnQkFDOUMsSUFBSSxFQUFFLEdBQUcsQ0FBQyxJQUFZO2FBQ3ZCLENBQUMsQ0FBQztRQUNMLENBQUM7SUFDSCxDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsYUFBYSxDQUNqQyxPQUFpQixFQUNqQixNQUEyQixFQUMzQixPQUtDO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxpQkFBaUIsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxhQUFhLEVBQUUsT0FBTyxFQUFFLElBQUksQ0FBQyxDQUFDO0lBQzdGLE1BQU0sTUFBTSxHQUFHLElBQUksR0FBRyxFQUFxQixDQUFDO0lBRTVDLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSSxFQUFFLENBQUM7UUFDdkIsTUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxhQUFhLEVBQUUsTUFBTSxDQUFFLENBQUM7UUFDaEQsSUFBSSxPQUFPLEVBQUUsSUFBSSxJQUFJLElBQUksS0FBSyxPQUFPLENBQUMsSUFBSTtZQUFFLFNBQVM7UUFDckQsTUFBTSxHQUFHLEdBQUcsR0FBRyxHQUFHLENBQUMsYUFBYSxJQUFJLEdBQUcsQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUNyRCxJQUFJLElBQUksR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQzNCLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNWLElBQUksR0FBRztnQkFDTCxhQUFhLEVBQUUsR0FBRyxDQUFDLGFBQWE7Z0JBQ2hDLElBQUk7Z0JBQ0osU0FBUyxFQUFFLEdBQUcsQ0FBQyxVQUFVLElBQUksRUFBRTtnQkFDL0IsWUFBWSxFQUFFLENBQUM7Z0JBQ2YsWUFBWSxFQUFFLENBQUM7Z0JBQ2YsU0FBUyxFQUFFLENBQUM7Z0JBQ1osU0FBUyxFQUFFLEdBQUcsQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUM7Z0JBQzlDLFFBQVEsRUFBRSxHQUFHLENBQUMsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDO2dCQUM3QyxRQUFRLEVBQUUsRUFBRTtnQkFDWixZQUFZLEVBQUUsQ0FBQzthQUNoQixDQUFDO1lBQ0YsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDeEIsQ0FBQztRQUNELE1BQU0sT0FBTyxHQUFHLENBQUMsSUFBSSxLQUFLLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksS0FBSyxPQUFPLENBQUMsQ0FBQztRQUMvRCxJQUFJLE9BQU87WUFBRSxJQUFJLENBQUMsWUFBWSxJQUFJLEdBQUcsQ0FBQyxXQUFXLENBQUM7O1lBQzdDLElBQUksQ0FBQyxZQUFZLElBQUksR0FBRyxDQUFDLFdBQVcsQ0FBQztRQUMxQyxJQUFJLENBQUMsUUFBUSxHQUFHLEdBQUcsQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNwRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQztZQUFFLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUM1RSxDQUFDO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxTQUFTLEVBQUUsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLG9CQUFvQixDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUNwRyxNQUFNLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLGFBQWEsSUFBSSxDQUFDLENBQUMsU0FBUyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRXJGLE1BQU0sS0FBSyxHQUFnQixFQUFFLENBQUM7SUFDOUIsS0FBSyxNQUFNLElBQUksSUFBSSxNQUFNLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQztRQUNuQyxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQztRQUN2RCxNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLGFBQWEsSUFBSSxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUMsQ0FBQztRQUN2RSxJQUFJLEtBQUssRUFBRSxDQUFDO1lBQ1YsSUFBSSxDQUFDLFlBQVksR0FBSSxLQUFLLENBQUMsWUFBdUIsSUFBSSxDQUFDLENBQUM7WUFDeEQsSUFBSSxDQUFDLE9BQU8sR0FBRyxLQUFLLENBQUMsT0FBNkIsQ0FBQztRQUNyRCxDQUFDO1FBQ0QsSUFBSSxJQUFJLENBQUMsU0FBUyxLQUFLLENBQUMsSUFBSSxPQUFPLEVBQUUsY0FBYztZQUFFLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEUsQ0FBQztJQUNELE9BQU8sS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsYUFBYSxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztBQUN4SCxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxRQUFRLENBQzVCLE9BQWlCLEVBQ2pCLE1BQTJCLEVBQzNCLElBQWUsRUFDZixJQUFVLEVBQ1YsYUFBdUIsQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQztJQUVuQyxNQUFNLEtBQUssR0FBRyxNQUFNLGFBQWEsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7SUFDbkUsTUFBTSxPQUFPLEdBQW1CO1FBQzlCLEdBQUcsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDbEMsS0FBSyxFQUFFLEtBQUssS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLEdBQUcsVUFBVSxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFO1lBQ3pFLFVBQVUsRUFBRSxJQUFJO1lBQ2hCLEtBQUssRUFBRSxFQUFpQjtZQUN4QixVQUFVLEVBQUUsQ0FBQztTQUNkLENBQUMsQ0FBQztRQUNILEVBQUUsS0FBSyxFQUFFLEtBQUssVUFBVSxDQUFDLFVBQVUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLEVBQUUsRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxFQUFFLEVBQUUsVUFBVSxFQUFFLENBQUMsRUFBRTtLQUNoRyxDQUFDO0lBQ0YsS0FBSyxNQUFNLElBQUksSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUN6QixNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsT0FBTyxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUM7UUFDOUMsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsR0FBRyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFHLFVBQVUsQ0FBQyxDQUFDLENBQUM7UUFDcEcsTUFBTSxNQUFNLEdBQ1YsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsRUFBRSxFQUFFLENBQUMsU0FBUyxDQUFDLFVBQVUsS0FBSyxJQUFJLElBQUksT0FBTyxJQUFJLFNBQVMsQ0FBQyxVQUFVLENBQUM7WUFDN0YsT0FBTyxDQUFDLE9BQU8sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDOUIsTUFBTSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDeEIsTUFBTSxDQUFDLFVBQVUsSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDO0lBQ3RDLENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGdCQUFnQixDQUNwQyxPQUFpQixFQUNqQixNQUEyQixFQUMzQixJQUFXO0lBRVgsTUFBTSxJQUFJLEdBQUcsTUFBTSxpQkFBaUIsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUN2RSxJQUFJLFlBQVksR0FBRyxDQUFDLENBQUM7SUFDckIsSUFBSSxjQUFjLEdBQUcsQ0FBQyxDQUFDO0lBQ3ZCLE1BQU0sY0FBYyxHQUFHLElBQUksR0FBRyxFQUFVLENBQUM7SUFDekMsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLEdBQUcsRUFBVSxDQUFDO0lBQzNDLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSSxFQUFFLENBQUM7UUFDdkIsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLElBQUksS0FBSyxPQUFPLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLFdBQVcsQ0FBQztRQUN6RSxJQUFJLE1BQU0sQ0FBQyxHQUFHLENBQUMsYUFBYSxFQUFFLE1BQU0sQ0FBQyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ25ELFlBQVksSUFBSSxNQUFNLENBQUM7WUFDdkIsY0FBYyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDeEMsQ0FBQzthQUFNLENBQUM7WUFDTixjQUFjLElBQUksTUFBTSxDQUFDLENBQUMsMkNBQTJDO1lBQ3JFLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDMUMsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPO1FBQ0wsWUFBWTtRQUNaLGNBQWM7UUFDZCxjQUFjLEVBQUUsY0FBYyxDQUFDLElBQUk7UUFDbkMsZ0JBQWdCLEVBQUUsZ0JBQWdCLENBQUMsSUFBSTtLQUN4QyxDQUFDO0FBQ0osQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsV0FBVyxDQUFDLE1BTWpDO0lBQ0MsTUFBTSxNQUFNLEdBQTRCLEVBQUUsU0FBUyxFQUFFLElBQUksSUFBSSxFQUFFLEVBQUUsQ0FBQztJQUNsRSxJQUFJLE1BQU0sQ0FBQyxZQUFZLEtBQUssU0FBUztRQUFFLE1BQU0sQ0FBQyxZQUFZLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQztJQUNqRixJQUFJLE1BQU0sQ0FBQyxPQUFPLEtBQUssU0FBUztRQUFFLE1BQU0sQ0FBQyxPQUFPLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQztJQUNsRSxNQUFNLFNBQVMsRUFBRTtTQUNkLE9BQU8sQ0FBQyxVQUFVLENBQUMsb0JBQW9CLENBQUM7U0FDeEMsU0FBUyxDQUNSLEVBQUUsT0FBTyxFQUFFLE1BQU0sQ0FBQyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sQ0FBQyxhQUFhLEVBQUUsU0FBUyxFQUFFLE1BQU0sQ0FBQyxTQUFTLEVBQUUsRUFDN0YsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEVBQ2hCLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUNqQixDQUFDO0FBQ04sQ0FBQyJ9
@@ -0,0 +1,31 @@
1
+ import type { IAccountPolicy } from './core/index.js';
2
+ import type { TSKRType } from './skr.types.js';
3
+ export interface IUstvaMovement {
4
+ accountNumber: string;
5
+ /** debitCents - creditCents over the period */
6
+ signedCents: number;
7
+ }
8
+ export interface IUstvaScenarioBases {
9
+ /** igE 19 % net bases (Kz 89) */
10
+ intraEuAcquisitionStandardCents: number;
11
+ /** igE 7 % net bases (Kz 93) */
12
+ intraEuAcquisitionReducedCents: number;
13
+ /**
14
+ * §13b bases (Kz 46). The scenario tag does not distinguish EU services
15
+ * from other §13b cases — everything lands here; reclassify to Kz 84
16
+ * manually if a non-EU-services §13b case ever occurs.
17
+ */
18
+ reverseChargeServicesEuCents: number;
19
+ }
20
+ export interface IUstvaPeriodData {
21
+ movements: IUstvaMovement[];
22
+ scenarioBases: IUstvaScenarioBases;
23
+ /** tagged igE/§13b entries examined */
24
+ taggedEntryCount: number;
25
+ /** extraction anomalies — review before filing */
26
+ warnings: string[];
27
+ }
28
+ export declare function getUstvaPeriodData(skrType: TSKRType, policy: IAccountPolicy, options: {
29
+ dateFrom: Date;
30
+ dateTo: Date;
31
+ }): Promise<IUstvaPeriodData>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * UStVA period extraction: one period's account movements plus the EXACT
3
+ * scenario Bemessungsgrundlagen (igE / §13b) taken from taxScenario-tagged
4
+ * journal entries. The result feeds @fin.cx/tax `computeUstva()` directly —
5
+ * movements as `IAccountMovementInput[]`, scenarioBases as
6
+ * `IScenarioBaseInput`.
7
+ *
8
+ * Base extraction per tagged entry: sum of (debit - credit) over lines that
9
+ * sit neither on a policy VAT account nor on a personal account (>= 10000).
10
+ * For the recipe shapes that is exactly the net expense line; Stornos flip
11
+ * sides and cancel. Every extracted base is cross-checked against the
12
+ * entry's own input-VAT line — disagreement lands in `warnings`.
13
+ */
14
+ import { vatFromNetCents } from '@fin.cx/calculation';
15
+ import { getDbSync } from './skr.database.js';
16
+ import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
17
+ import { aggregateTrialBalance } from './skr.balances.js';
18
+ function isPersonalAccount(accountNumber) {
19
+ const numeric = Number.parseInt(accountNumber, 10);
20
+ return numeric >= 10000 && numeric <= 99999;
21
+ }
22
+ export async function getUstvaPeriodData(skrType, policy, options) {
23
+ const trialBalance = await aggregateTrialBalance(skrType, options);
24
+ const movements = trialBalance.accounts
25
+ .filter((account) => account.balanceCents !== 0)
26
+ .map((account) => ({
27
+ accountNumber: account.accountNumber,
28
+ signedCents: account.balanceCents,
29
+ }));
30
+ const vatAccounts = new Set(Object.values(policy.vat));
31
+ const warnings = [];
32
+ const scenarioBases = {
33
+ intraEuAcquisitionStandardCents: 0,
34
+ intraEuAcquisitionReducedCents: 0,
35
+ reverseChargeServicesEuCents: 0,
36
+ };
37
+ const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
38
+ const tagged = (await collection
39
+ .find({
40
+ schemaVersion: 2,
41
+ skrType,
42
+ status: { $in: ['posted', 'finalized'] },
43
+ taxScenario: { $in: ['intra_eu_acquisition', 'reverse_charge_13b'] },
44
+ date: { $gte: options.dateFrom, $lte: options.dateTo },
45
+ }, { projection: { sequenceNumber: 1, journalNumber: 1, taxScenario: 1, lines: 1 } })
46
+ .toArray());
47
+ for (const entry of tagged) {
48
+ const label = entry.journalNumber ?? `seq ${entry.sequenceNumber}`;
49
+ const inputVatAccount = entry.taxScenario === 'intra_eu_acquisition'
50
+ ? policy.vat.intraEuAcqInput
51
+ : policy.vat.reverseChargeInput;
52
+ let baseCents = 0;
53
+ let inputVatCents = 0;
54
+ for (const line of entry.lines) {
55
+ const amount = line.amountCents ?? 0;
56
+ const signed = line.side === 'debit' ? amount : -amount;
57
+ if (line.accountNumber === inputVatAccount) {
58
+ inputVatCents += signed;
59
+ continue;
60
+ }
61
+ if (vatAccounts.has(line.accountNumber))
62
+ continue;
63
+ if (isPersonalAccount(line.accountNumber))
64
+ continue;
65
+ baseCents += signed;
66
+ }
67
+ if (baseCents === 0 && inputVatCents !== 0) {
68
+ warnings.push(`entry ${label}: no base extractable (counterparty not a personal account?) — VAT line is ${inputVatCents} cents`);
69
+ continue;
70
+ }
71
+ if (baseCents === 0)
72
+ continue;
73
+ // rate detection from the entry's own VAT line (19 % vs 7 %)
74
+ const absBase = Math.abs(baseCents);
75
+ const absVat = Math.abs(inputVatCents);
76
+ const reduced = Math.abs(absVat * 100 - 7 * absBase) < Math.abs(absVat * 100 - 19 * absBase);
77
+ const rate = reduced ? 7 : 19;
78
+ const expectedVat = vatFromNetCents(baseCents, rate);
79
+ if (Math.abs(inputVatCents - expectedVat) > 2) {
80
+ warnings.push(`entry ${label}: base ${baseCents} cents at ${rate} % expects VAT ${expectedVat}, ledger line says ${inputVatCents}`);
81
+ }
82
+ if (entry.taxScenario === 'intra_eu_acquisition') {
83
+ if (reduced)
84
+ scenarioBases.intraEuAcquisitionReducedCents += baseCents;
85
+ else
86
+ scenarioBases.intraEuAcquisitionStandardCents += baseCents;
87
+ }
88
+ else {
89
+ scenarioBases.reverseChargeServicesEuCents += baseCents;
90
+ }
91
+ }
92
+ return {
93
+ movements,
94
+ scenarioBases,
95
+ taggedEntryCount: tagged.length,
96
+ warnings,
97
+ };
98
+ }
99
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2tyLnVzdHZhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc2tyLnVzdHZhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7R0FZRztBQUNILE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDM0QsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUF1QzFELFNBQVMsaUJBQWlCLENBQUMsYUFBcUI7SUFDOUMsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxhQUFhLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDbkQsT0FBTyxPQUFPLElBQUksS0FBSyxJQUFJLE9BQU8sSUFBSSxLQUFLLENBQUM7QUFDOUMsQ0FBQztBQUVELE1BQU0sQ0FBQyxLQUFLLFVBQVUsa0JBQWtCLENBQ3RDLE9BQWlCLEVBQ2pCLE1BQXNCLEVBQ3RCLE9BQXlDO0lBRXpDLE1BQU0sWUFBWSxHQUFHLE1BQU0scUJBQXFCLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ25FLE1BQU0sU0FBUyxHQUFxQixZQUFZLENBQUMsUUFBUTtTQUN0RCxNQUFNLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDLE9BQU8sQ0FBQyxZQUFZLEtBQUssQ0FBQyxDQUFDO1NBQy9DLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNqQixhQUFhLEVBQUUsT0FBTyxDQUFDLGFBQWE7UUFDcEMsV0FBVyxFQUFFLE9BQU8sQ0FBQyxZQUFZO0tBQ2xDLENBQUMsQ0FBQyxDQUFDO0lBRU4sTUFBTSxXQUFXLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsR0FBRyxD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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/skr",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
5
5
  "main": "dist_ts/index.js",
6
6
  "typings": "dist_ts/index.d.ts",
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@fin.cx/chartdata": "^2.1.0",
38
+ "@fin.cx/tax": "^1.1.0",
38
39
  "@git.zone/tsbuild": "^4.4.2",
39
40
  "@git.zone/tsrun": "^2.0.5",
40
41
  "@git.zone/tstest": "^3.6.6",
package/readme.plan.md CHANGED
@@ -102,6 +102,19 @@ allowed BU keys, account→UStVA-Kennzahl mappings, scenario Kennzahlen table
102
102
  (89/93, 46/47, 84/85, 41, 43, 81, 86, 60, 66/61/67/62), coverage introspection,
103
103
  provenance per record, reproducible seed pipeline in tools/.
104
104
  **RELEASED as @fin.cx/chartdata v1.1.0** (2026-07-09, verdaccio + npmjs).
105
+ **A1 REBASE COMPLETE (2026-07-09):** chartdata v2.1.0 = curated oracle-verified
106
+ datasets (91 SKR03 / 78 SKR04 accounts, verification-only oracle, no foreign
107
+ strings); skr v3.0.0 aligned: charts GENERATED from chartdata
108
+ (tools/generate-chart-data.ts), policies corrected (expense default 4900/6300,
109
+ SKR04 controls 1200/3300, bank 1800/Kasse 1600), all 98 tests remapped to real
110
+ DATEV numbers and green. A2 HGB §266/§275 mappings DONE
111
+ (chartdata v2.2.0: every non-statistical account carries hgb266Position or
112
+ hgb275Position, validated against the statute Gliederung trees in
113
+ chartdata.hgb.ts and verified against the oracle's position tags — 11
114
+ documented modeling exceptions where German textbook treatment differs, e.g.
115
+ USt liabilities in Passiva C.8). NEXT: A3 E-Bilanz taxonomy; D1 can now build
116
+ the §266/§275 Gliederung from chartdata.
117
+
105
118
  **A1 cross-diff done (2026-07-09):** Odoo l10n_de oracle proves the skr-v1 seed
106
119
  charts are an invented hybrid (~50/125 SKR03 + ~66/130 SKR04 divergences);
107
120
  ALL fiscal A1 additions verified clean. Rate-label fixes released (v1.1.1:
@@ -132,9 +145,30 @@ auto/review/unmatched decisions to skr recipes in the consuming app.
132
145
 
133
146
  B1 `@fin.cx/camt` (CAMT.052/053/054; MT940 = legacy) + `@fin.cx/bankrec` (canonical
134
147
  decimal bank txn with SEPA refs, adapters, balance-chain continuity, confidence-scored
135
- matching, auto-post above threshold, review queue below). B2 OPOS inside skr (ts/opos/:
148
+ matching, auto-post above threshold, review queue below).
149
+
150
+ **B2 status 2026-07-09: RELEASED in skr v3.1.0** — open items DERIVED from the
151
+ hashed v2 ledger (no drifting side-state): grouped by (personal account,
152
+ belegfeld1), partial settlements book only the paid amount, full settlements
153
+ with Skonto+VSt correction via the core recipe (ordinary audited postings),
154
+ aging buckets, dunning metadata (OposMeta, process state only), control totals
155
+ = aggregated personal ranges (feed HGB B.II.1/C.4 in D1). Dunning LETTERS out
156
+ of scope as planned. A2 also DONE (chartdata v2.2.0 HGB mappings).
157
+
158
+ B2 original scope: OPOS inside skr (ts/opos/:
136
159
  masters, open items, clearItems with Skonto+VAT correction, aging, dunning states;
137
- invariant: subledger == control account). B3 `@fin.cx/assets` (AfA schedules from BMF
160
+ invariant: subledger == control account).
161
+
162
+ **B3 status 2026-07-09: RELEASED as @fin.cx/assets v1.1.0** — pure-function
163
+ Anlagenbuchhaltung: exact-cent AfA schedules (linear + degressive with switch,
164
+ pro rata temporis), GWG immediate/Sammelposten, year-keyed policy data (GWG
165
+ thresholds, degressive windows incl. Investitionsbooster 2025, BMF useful-life
166
+ suggestions), disposals with Buchgewinn/-verlust, §284(3) Anlagespiegel (gross
167
+ method, tie-outs tested), ledger-agnostic posting proposals for skr recipes.
168
+ 12/12 tests. Remaining M-B: A3 taxonomy, then D1 consumes Anlagespiegel +
169
+ getControlTotals + chartdata HGB mappings.
170
+
171
+ B3 original scope: `@fin.cx/assets` (AfA schedules from BMF
138
172
  tables, GWG/degressive as year-keyed policy, Anlagespiegel → D1). B1 before live UStVA;
139
173
  B2/B3 may trail.
140
174
 
@@ -146,8 +180,32 @@ version-compat matrix + checksum gate). Kennzahl computation in `@fin.cx/tax` (V
146
180
  ships early). C1 bridge → C2 UStVA 3-month parallel-run (3 consecutive zero/explained-diff
147
181
  months required) → C2' live + E1 → C3 DFV/ZM/UStJE.
148
182
 
183
+ **UStVA Kennzahl module RELEASED 2026-07-09 as @fin.cx/tax v1.1.0** —
184
+ `computeUstva()`: pure aggregation of period account movements into UStVA
185
+ Kennzahlen via chartdata mappings (81/86/41/43/48/60 bases, 66/61/67/62 taxes),
186
+ form rounding done right (Bemessungsgrundlagen truncated to full euros on the
187
+ period sum per Kennzahl; taxes cent-exact; output tax = form computation from
188
+ truncated bases), igE/§13b bases from exact `scenarioBases` (skr taxScenario
189
+ tags) with `(derived)` fallback from output-tax movements, Kz 47/85 split
190
+ preserving the ledger total, Kz 83 Zahllast + Kz 39 Sondervorauszahlung,
191
+ ledger-vs-form reconciliation with tolerance (default 5 EUR) and loud `issues`
192
+ (unmapped revenue, unknown revenue-class accounts, undifferentiated output
193
+ VAT). Soll/Ist is the CALLER's period selection; `taxationMethod` recorded on
194
+ the result — open question #3 still needs answering before first filing.
195
+ 12/12 tests. Remaining C2: skr-side glue (movements + scenario base extraction
196
+ per period), then parallel-run diffing once ELSTER access exists.
197
+
149
198
  ### M-D — Year-end (`@fin.cx/jahresabschluss` + `@fin.cx/tax`, Q1–Q2 2027)
150
199
 
200
+ **D1 CORE RELEASED 2026-07-09 as @fin.cx/jahresabschluss v1.1.0** (built early
201
+ — all inputs existed): §266 Bilanz + §275 GKV GuV from chartdata mappings and
202
+ exact-cent balances (loud failure on unmapped balances; Jahresergebnis derived
203
+ twice and tied out on Passiva A.V; personal accounts auto-roll to B.II.1/C.4),
204
+ §267/§267a size classes as year-keyed data (post-2024 thresholds + two-year
205
+ rule), condensed statements for klein/kleinst. 10/10 tests. Remaining D1:
206
+ Anhang skeleton, Anlagespiegel integration glue, real-data validation
207
+ ("re-file the past" needs prior-year books).
208
+
151
209
  D1 §266/§275 Gliederung + §267 size classes + Anhang skeleton + Anlagespiegel. D3 tax
152
210
  engine (§4(5) EStG, §8b KStG, GewSt §8/§9 add-backs, loss carryforwards, KSt 15%+Soli,
153
211
  GewSt Hebesatz Bremen 460%; provisions posted back to skr). D2 E-Bilanz XBRL writer
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/skr',
6
- version: '3.0.0',
6
+ version: '3.2.0',
7
7
  description: 'SKR03 and SKR04 German accounting standards for double-entry bookkeeping'
8
8
  }
package/ts/index.ts CHANGED
@@ -19,6 +19,19 @@ export type {
19
19
  ITrialBalanceCents,
20
20
  IBalanceQueryOptions,
21
21
  } from './skr.balances.js';
22
+ export {
23
+ listOpenItems,
24
+ getAging,
25
+ getControlTotals,
26
+ setOposMeta,
27
+ } from './skr.opos.js';
28
+ export type { IOpenItem, IAgingBucket, IControlTotals, TOposKind } from './skr.opos.js';
29
+ export { getUstvaPeriodData } from './skr.ustva.js';
30
+ export type {
31
+ IUstvaPeriodData,
32
+ IUstvaMovement,
33
+ IUstvaScenarioBases,
34
+ } from './skr.ustva.js';
22
35
  export { verifyHashChain } from './skr.verify.js';
23
36
  export type { IChainVerificationResult } from './skr.verify.js';
24
37
  export { migrateToV2 } from './skr.migrate.js';
package/ts/skr.api.ts CHANGED
@@ -1026,6 +1026,102 @@ export class SkrApi {
1026
1026
  return migrateToV2();
1027
1027
  }
1028
1028
 
1029
+ // ========== OPOS (open items) ==========
1030
+
1031
+ public async listOpenItems(options?: {
1032
+ kind?: import('./skr.opos.js').TOposKind;
1033
+ accountNumber?: string;
1034
+ asOf?: Date;
1035
+ includeSettled?: boolean;
1036
+ }): Promise<import('./skr.opos.js').IOpenItem[]> {
1037
+ this.ensureInitialized();
1038
+ const { listOpenItems } = await import('./skr.opos.js');
1039
+ return listOpenItems(this.currentSKRType!, this.getDefaultPolicy(), options);
1040
+ }
1041
+
1042
+ public async getOposAging(
1043
+ kind: import('./skr.opos.js').TOposKind,
1044
+ asOf: Date,
1045
+ bucketDays?: number[],
1046
+ ): Promise<import('./skr.opos.js').IAgingBucket[]> {
1047
+ this.ensureInitialized();
1048
+ const { getAging } = await import('./skr.opos.js');
1049
+ return getAging(this.currentSKRType!, this.getDefaultPolicy(), kind, asOf, bucketDays);
1050
+ }
1051
+
1052
+ public async getControlTotals(asOf?: Date): Promise<import('./skr.opos.js').IControlTotals> {
1053
+ this.ensureInitialized();
1054
+ const { getControlTotals } = await import('./skr.opos.js');
1055
+ return getControlTotals(this.currentSKRType!, this.getDefaultPolicy(), asOf);
1056
+ }
1057
+
1058
+ // ========== UStVA extraction ==========
1059
+
1060
+ /**
1061
+ * One period's movements + exact igE/§13b scenario bases, shaped for
1062
+ * @fin.cx/tax `computeUstva()`. Review `warnings` before filing.
1063
+ */
1064
+ public async getUstvaPeriodData(
1065
+ dateFrom: Date,
1066
+ dateTo: Date,
1067
+ ): Promise<import('./skr.ustva.js').IUstvaPeriodData> {
1068
+ this.ensureInitialized();
1069
+ const { getUstvaPeriodData } = await import('./skr.ustva.js');
1070
+ return getUstvaPeriodData(this.currentSKRType!, this.getDefaultPolicy(), {
1071
+ dateFrom,
1072
+ dateTo,
1073
+ });
1074
+ }
1075
+
1076
+ public async setDunning(params: {
1077
+ accountNumber: string;
1078
+ reference: string;
1079
+ dunningLevel?: number;
1080
+ dueDate?: string;
1081
+ }): Promise<void> {
1082
+ this.ensureInitialized();
1083
+ const { setOposMeta } = await import('./skr.opos.js');
1084
+ return setOposMeta({ skrType: this.currentSKRType!, ...params });
1085
+ }
1086
+
1087
+ /**
1088
+ * Settle an open item with a bank payment (Skonto and small-difference
1089
+ * write-offs via the core recipe); ordinary v2 posting, fully audited.
1090
+ */
1091
+ public async settleOpenItem(params: {
1092
+ item: import('./skr.opos.js').IOpenItem;
1093
+ paidCents: number;
1094
+ date: Date;
1095
+ bankAccount?: string;
1096
+ skonto?: { ratePercent: number };
1097
+ diffToleranceCents?: number;
1098
+ description?: string;
1099
+ }): Promise<JournalEntry> {
1100
+ this.ensureInitialized();
1101
+ // a plain underpayment (no Skonto, beyond tolerance) is a PARTIAL
1102
+ // settlement: book only the paid amount and leave the rest open
1103
+ const shortfall = params.item.openCents - params.paidCents;
1104
+ const isPartial =
1105
+ !params.skonto && shortfall > (params.diffToleranceCents ?? 0);
1106
+ const draft = core.bookBankPayment(
1107
+ {
1108
+ date: params.date,
1109
+ direction: params.item.kind === 'debtor' ? 'incoming' : 'outgoing',
1110
+ bankAccount: params.bankAccount,
1111
+ counterpartyAccount: params.item.accountNumber,
1112
+ expectedCents: isPartial ? params.paidCents : params.item.openCents,
1113
+ paidCents: params.paidCents,
1114
+ skonto: params.skonto,
1115
+ diffToleranceCents: params.diffToleranceCents,
1116
+ description:
1117
+ params.description ?? `Ausgleich ${params.item.reference || params.item.accountNumber}`,
1118
+ reference: params.item.reference || undefined,
1119
+ },
1120
+ this.getDefaultPolicy(),
1121
+ );
1122
+ return this.journalPoster.postDraft(draft, { policy: this.getDefaultPolicy() });
1123
+ }
1124
+
1029
1125
  /**
1030
1126
  * Get current SKR type
1031
1127
  */