@fin.cx/tax 1.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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export declare const commitinfo: {
5
+ name: string;
6
+ version: string;
7
+ description: string;
8
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export const commitinfo = {
5
+ name: '@fin.cx/tax',
6
+ version: '1.1.0',
7
+ description: 'German tax computation engine: UStVA Kennzahl aggregation from ledger balances with form rounding, Zahllast and reconciliation checks — exact cents'
8
+ };
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxhQUFhO0lBQ25CLE9BQU8sRUFBRSxPQUFPO0lBQ2hCLFdBQVcsRUFBRSxxSkFBcUo7Q0FDbkssQ0FBQSJ9
@@ -0,0 +1,3 @@
1
+ export * from './ustva.types.js';
2
+ export * from './ustva.accounts.js';
3
+ export * from './ustva.js';
@@ -0,0 +1,4 @@
1
+ export * from './ustva.types.js';
2
+ export * from './ustva.accounts.js';
3
+ export * from './ustva.js';
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxZQUFZLENBQUMifQ==
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Output-VAT ledger accounts per chart. These accounts carry no UStVA
3
+ * Kennzahl in @fin.cx/chartdata (output tax is computed by the form from
4
+ * the bases); the tax module needs them for reconciliation and for deriving
5
+ * igE/§13b bases when no scenario bases are supplied.
6
+ *
7
+ * Numbers match the @fin.cx/skr account policies (SKR03/SKR04 defaults).
8
+ */
9
+ import type { TChartName } from '@fin.cx/chartdata';
10
+ import type { IVatAccountSet } from './ustva.types.js';
11
+ export declare const VAT_ACCOUNTS: Record<TChartName, IVatAccountSet>;
@@ -0,0 +1,17 @@
1
+ export const VAT_ACCOUNTS = {
2
+ SKR03: {
3
+ outputStandard: ['1776'],
4
+ outputReduced: ['1771'],
5
+ outputGeneric: ['1770'],
6
+ outputIntraEuAcquisition: ['1784'],
7
+ outputReverseCharge: ['1787'],
8
+ },
9
+ SKR04: {
10
+ outputStandard: ['3806'],
11
+ outputReduced: ['3801'],
12
+ outputGeneric: ['3800'],
13
+ outputIntraEuAcquisition: ['3804'],
14
+ outputReverseCharge: ['3837'],
15
+ },
16
+ };
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXN0dmEuYWNjb3VudHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy91c3R2YS5hY2NvdW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFXQSxNQUFNLENBQUMsTUFBTSxZQUFZLEdBQXVDO0lBQzlELEtBQUssRUFBRTtRQUNMLGNBQWMsRUFBRSxDQUFDLE1BQU0sQ0FBQztRQUN4QixhQUFhLEVBQUUsQ0FBQyxNQUFNLENBQUM7UUFDdkIsYUFBYSxFQUFFLENBQUMsTUFBTSxDQUFDO1FBQ3ZCLHdCQUF3QixFQUFFLENBQUMsTUFBTSxDQUFDO1FBQ2xDLG1CQUFtQixFQUFFLENBQUMsTUFBTSxDQUFDO0tBQzlCO0lBQ0QsS0FBSyxFQUFFO1FBQ0wsY0FBYyxFQUFFLENBQUMsTUFBTSxDQUFDO1FBQ3hCLGFBQWEsRUFBRSxDQUFDLE1BQU0sQ0FBQztRQUN2QixhQUFhLEVBQUUsQ0FBQyxNQUFNLENBQUM7UUFDdkIsd0JBQXdCLEVBQUUsQ0FBQyxNQUFNLENBQUM7UUFDbEMsbUJBQW1CLEVBQUUsQ0FBQyxNQUFNLENBQUM7S0FDOUI7Q0FDRixDQUFDIn0=
@@ -0,0 +1,2 @@
1
+ import type { IUstvaInput, IUstvaResult } from './ustva.types.js';
2
+ export declare function computeUstva(input: IUstvaInput): IUstvaResult;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * UStVA Kennzahl computation — pure, ledger-agnostic.
3
+ *
4
+ * Input: period account movements in exact cents (debit - credit, as
5
+ * @fin.cx/skr's trial-balance aggregation delivers), plus optional exact
6
+ * scenario bases from taxScenario-tagged entries. Mapping authority is
7
+ * @fin.cx/chartdata (account→Kennzahl); this module adds the form
8
+ * arithmetic:
9
+ *
10
+ * - Bemessungsgrundlagen are declared in FULL EUROS, cents truncated toward
11
+ * zero — truncation applies to the period sum per Kennzahl, not per booking.
12
+ * - Tax amounts (Vorsteuer, §13b tax) are cent-exact.
13
+ * - Output tax on domestic/igE bases is what the form computes from the
14
+ * truncated bases (base EUR × 19 = cents, exactly integer), NOT the ledger
15
+ * balance; the difference is surfaced in the reconciliation.
16
+ * - Kz 83 (Zahllast) is computed the way the form computes it.
17
+ *
18
+ * Unattributable movements FAIL LOUDLY via `issues` — a filing must not be
19
+ * generated while issues is non-empty.
20
+ */
21
+ import { vatFromNetCents } from '@fin.cx/calculation';
22
+ import { getChart } from '@fin.cx/chartdata';
23
+ import { VAT_ACCOUNTS } from './ustva.accounts.js';
24
+ const KENNZAHL_LABELS = {
25
+ 81: 'Steuerpflichtige Umsätze 19 %',
26
+ 86: 'Steuerpflichtige Umsätze 7 %',
27
+ 89: 'Innergemeinschaftliche Erwerbe 19 %',
28
+ 93: 'Innergemeinschaftliche Erwerbe 7 %',
29
+ 41: 'Steuerfreie innergemeinschaftliche Lieferungen',
30
+ 43: 'Steuerfreie Umsätze mit Vorsteuerabzug (u. a. Ausfuhr)',
31
+ 48: 'Steuerfreie Umsätze ohne Vorsteuerabzug',
32
+ 46: 'Sonstige Leistungen aus dem übrigen Gemeinschaftsgebiet (§13b)',
33
+ 47: 'Steuer zu Kennzahl 46',
34
+ 84: 'Andere Leistungen eines im Ausland ansässigen Unternehmers (§13b)',
35
+ 85: 'Steuer zu Kennzahl 84',
36
+ 60: 'Steuerpflichtige Umsätze, für die der Leistungsempfänger die Steuer schuldet (§13b)',
37
+ 66: 'Vorsteuer aus Rechnungen anderer Unternehmer',
38
+ 61: 'Vorsteuer aus innergemeinschaftlichem Erwerb',
39
+ 62: 'Entrichtete Einfuhrumsatzsteuer',
40
+ 67: 'Vorsteuer aus Leistungen im Sinne des §13b UStG',
41
+ 39: 'Anrechnung der Sondervorauszahlung (Dauerfristverlängerung)',
42
+ 83: 'Verbleibende Umsatzsteuer-Vorauszahlung / Überschuss',
43
+ };
44
+ /** result order follows the form sections */
45
+ const KENNZAHL_ORDER = [81, 86, 89, 93, 41, 43, 48, 46, 47, 84, 85, 60, 66, 61, 62, 67, 39, 83];
46
+ /** full euros, cents truncated toward zero (form rule for bases) */
47
+ function truncateToEur(cents) {
48
+ return Math.trunc(cents / 100);
49
+ }
50
+ /** cents of `ratePercent` on a full-euro base — exact integer arithmetic */
51
+ function taxOnEurBase(baseEur, ratePercent) {
52
+ return baseEur * ratePercent;
53
+ }
54
+ function isRevenueClassNumber(chart, accountNumber) {
55
+ const firstDigit = accountNumber[0];
56
+ return chart === 'SKR03' ? firstDigit === '8' : firstDigit === '4';
57
+ }
58
+ export function computeUstva(input) {
59
+ const dataset = getChart(input.chart, input.year);
60
+ const byNumber = new Map(dataset.accounts.map((account) => [account.accountNumber, account]));
61
+ const vatAccounts = { ...VAT_ACCOUNTS[input.chart], ...input.vatAccounts };
62
+ const exempt = new Set(input.exemptAccounts ?? []);
63
+ const tolerance = input.reconciliationToleranceCents ?? 500;
64
+ const issues = [];
65
+ const checks = [];
66
+ // ---- pass 1: bucket the movements -------------------------------------
67
+ const baseCentsByKz = new Map();
68
+ const taxCentsByKz = new Map();
69
+ const accountsByKz = new Map();
70
+ const contribute = (bucket, kennzahl, cents, accountNumber) => {
71
+ bucket.set(kennzahl, (bucket.get(kennzahl) ?? 0) + cents);
72
+ const list = accountsByKz.get(kennzahl) ?? [];
73
+ if (!list.includes(accountNumber))
74
+ list.push(accountNumber);
75
+ accountsByKz.set(kennzahl, list);
76
+ };
77
+ let ledgerOutputStandardCents = 0;
78
+ let ledgerOutputReducedCents = 0;
79
+ let ledgerOutputGenericCents = 0;
80
+ let ledgerIgeOutputCents = 0;
81
+ let ledgerRcOutputCents = 0;
82
+ const unknownAccounts = [];
83
+ for (const movement of input.movements) {
84
+ if (movement.signedCents === 0)
85
+ continue;
86
+ const { accountNumber, signedCents } = movement;
87
+ // output tax accounts (no Kennzahl of their own)
88
+ if (vatAccounts.outputStandard.includes(accountNumber)) {
89
+ ledgerOutputStandardCents += -signedCents;
90
+ continue;
91
+ }
92
+ if (vatAccounts.outputReduced.includes(accountNumber)) {
93
+ ledgerOutputReducedCents += -signedCents;
94
+ continue;
95
+ }
96
+ if (vatAccounts.outputGeneric.includes(accountNumber)) {
97
+ ledgerOutputGenericCents += -signedCents;
98
+ continue;
99
+ }
100
+ if (vatAccounts.outputIntraEuAcquisition.includes(accountNumber)) {
101
+ ledgerIgeOutputCents += -signedCents;
102
+ continue;
103
+ }
104
+ if (vatAccounts.outputReverseCharge.includes(accountNumber)) {
105
+ ledgerRcOutputCents += -signedCents;
106
+ continue;
107
+ }
108
+ const account = byNumber.get(accountNumber);
109
+ if (!account) {
110
+ const numeric = Number.parseInt(accountNumber, 10);
111
+ if (numeric >= 10000 && numeric <= 99999)
112
+ continue; // personal accounts
113
+ if (isRevenueClassNumber(input.chart, accountNumber) && !exempt.has(accountNumber)) {
114
+ issues.push(`revenue-class account ${accountNumber} is unknown in ${input.chart}/${input.year} — cannot attribute its movement to a Kennzahl`);
115
+ }
116
+ else {
117
+ unknownAccounts.push(accountNumber);
118
+ }
119
+ continue;
120
+ }
121
+ if (account.ustva) {
122
+ if (account.ustva.role === 'base') {
123
+ // revenue bases carry credit balances -> base = credit surplus
124
+ contribute(baseCentsByKz, account.ustva.kennzahl, -signedCents, accountNumber);
125
+ }
126
+ else {
127
+ // Vorsteuer accounts are assets (debit balances)
128
+ const sign = account.accountType === 'asset' || account.accountType === 'expense' ? 1 : -1;
129
+ contribute(taxCentsByKz, account.ustva.kennzahl, sign * signedCents, accountNumber);
130
+ }
131
+ continue;
132
+ }
133
+ if (account.accountType === 'revenue' && !exempt.has(accountNumber)) {
134
+ issues.push(`revenue account ${accountNumber} (${account.accountName}) has a non-zero movement but no UStVA mapping — declare it exempt or map it`);
135
+ }
136
+ }
137
+ if (unknownAccounts.length > 0) {
138
+ checks.push(`accounts unknown to the chart, ignored for UStVA: ${unknownAccounts.join(', ')}`);
139
+ }
140
+ if (ledgerOutputGenericCents !== 0) {
141
+ issues.push(`undifferentiated output VAT account carries ${ledgerOutputGenericCents} cents — cannot be attributed to a rate; rebook to the 19 %/7 % accounts`);
142
+ }
143
+ // ---- scenario bases: igE and inbound §13b ------------------------------
144
+ const scenario = input.scenarioBases ?? {};
145
+ const derived = [];
146
+ let kz89Cents = scenario.intraEuAcquisitionStandardCents ?? 0;
147
+ let kz89Source = '(scenario)';
148
+ if (scenario.intraEuAcquisitionStandardCents === undefined && ledgerIgeOutputCents !== 0) {
149
+ kz89Cents = Math.round((ledgerIgeOutputCents * 100) / 19);
150
+ kz89Source = '(derived)';
151
+ derived.push(`Kz 89 derived from igE output tax movement (${ledgerIgeOutputCents} cents / 19 %)`);
152
+ }
153
+ const kz93Cents = scenario.intraEuAcquisitionReducedCents ?? 0;
154
+ let kz46Cents = scenario.reverseChargeServicesEuCents ?? 0;
155
+ let kz46Source = '(scenario)';
156
+ const kz84Cents = scenario.reverseChargeOtherCents ?? 0;
157
+ const hasRcScenario = scenario.reverseChargeServicesEuCents !== undefined ||
158
+ scenario.reverseChargeOtherCents !== undefined;
159
+ if (!hasRcScenario && ledgerRcOutputCents !== 0) {
160
+ kz46Cents = Math.round((ledgerRcOutputCents * 100) / 19);
161
+ kz46Source = '(derived)';
162
+ derived.push(`Kz 46 derived from §13b output tax movement (${ledgerRcOutputCents} cents / 19 %) — split vs Kz 84 unknown without scenario bases`);
163
+ }
164
+ // §13b tax split: keep the total identical to the ledger movement
165
+ let kz47Cents = 0;
166
+ let kz85Cents = 0;
167
+ if (kz46Cents !== 0 && kz84Cents !== 0) {
168
+ kz47Cents = vatFromNetCents(kz46Cents, 19);
169
+ kz85Cents = ledgerRcOutputCents - kz47Cents;
170
+ }
171
+ else if (kz84Cents !== 0) {
172
+ kz85Cents = ledgerRcOutputCents;
173
+ }
174
+ else if (kz46Cents !== 0) {
175
+ kz47Cents = ledgerRcOutputCents;
176
+ }
177
+ else if (ledgerRcOutputCents !== 0) {
178
+ kz47Cents = ledgerRcOutputCents; // derived path above set Kz 46
179
+ }
180
+ // consistency: scenario bases × rate must reproduce the ledger tax
181
+ if (kz89Source === '(scenario)' && kz89Cents !== 0) {
182
+ const expected = vatFromNetCents(kz89Cents, 19);
183
+ const diff = ledgerIgeOutputCents - expected;
184
+ checks.push(`igE 19 %: ledger output tax ${ledgerIgeOutputCents} vs 19 % of scenario base ${expected} (diff ${diff})`);
185
+ if (Math.abs(diff) > tolerance) {
186
+ issues.push(`igE scenario base and igE output tax ledger movement disagree by ${diff} cents`);
187
+ }
188
+ }
189
+ if (hasRcScenario) {
190
+ const expected = vatFromNetCents(kz46Cents, 19) + vatFromNetCents(kz84Cents, 19);
191
+ const diff = ledgerRcOutputCents - expected;
192
+ checks.push(`§13b: ledger output tax ${ledgerRcOutputCents} vs 19 % of scenario bases ${expected} (diff ${diff})`);
193
+ if (Math.abs(diff) > tolerance) {
194
+ issues.push(`§13b scenario bases and §13b output tax ledger movement disagree by ${diff} cents`);
195
+ }
196
+ }
197
+ checks.push(...derived);
198
+ // ---- assemble the Kennzahlen -------------------------------------------
199
+ const values = new Map();
200
+ const put = (kennzahl, kind, value, accounts) => {
201
+ if (value === 0 && accounts.length === 0)
202
+ return;
203
+ values.set(kennzahl, {
204
+ kennzahl,
205
+ kind,
206
+ value,
207
+ label: KENNZAHL_LABELS[kennzahl] ?? `Kennzahl ${kennzahl}`,
208
+ accounts,
209
+ });
210
+ };
211
+ for (const [kennzahl, cents] of baseCentsByKz) {
212
+ put(kennzahl, 'baseEur', truncateToEur(cents), accountsByKz.get(kennzahl) ?? []);
213
+ }
214
+ for (const [kennzahl, cents] of taxCentsByKz) {
215
+ put(kennzahl, 'taxCents', cents, accountsByKz.get(kennzahl) ?? []);
216
+ }
217
+ if (kz89Cents !== 0)
218
+ put(89, 'baseEur', truncateToEur(kz89Cents), [kz89Source]);
219
+ if (kz93Cents !== 0)
220
+ put(93, 'baseEur', truncateToEur(kz93Cents), ['(scenario)']);
221
+ if (kz46Cents !== 0)
222
+ put(46, 'baseEur', truncateToEur(kz46Cents), [kz46Source]);
223
+ if (kz47Cents !== 0)
224
+ put(47, 'taxCents', kz47Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
225
+ if (kz84Cents !== 0)
226
+ put(84, 'baseEur', truncateToEur(kz84Cents), ['(scenario)']);
227
+ if (kz85Cents !== 0)
228
+ put(85, 'taxCents', kz85Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
229
+ const sondervorauszahlung = input.sondervorauszahlungCents ?? 0;
230
+ if (sondervorauszahlung !== 0)
231
+ put(39, 'taxCents', sondervorauszahlung, ['(input)']);
232
+ const kzValue = (kennzahl) => values.get(kennzahl)?.value ?? 0;
233
+ // ---- form arithmetic ----------------------------------------------------
234
+ const formDomesticTaxCents = taxOnEurBase(kzValue(81), 19) + taxOnEurBase(kzValue(86), 7);
235
+ const formIgeTaxCents = taxOnEurBase(kzValue(89), 19) + taxOnEurBase(kzValue(93), 7);
236
+ const inputTaxCents = kzValue(66) + kzValue(61) + kzValue(67) + kzValue(62);
237
+ const zahllastCents = formDomesticTaxCents +
238
+ formIgeTaxCents +
239
+ kz47Cents +
240
+ kz85Cents -
241
+ inputTaxCents -
242
+ sondervorauszahlung;
243
+ put(83, 'taxCents', zahllastCents, ['(computed)']);
244
+ // ---- reconciliation ------------------------------------------------------
245
+ const ledgerOutputTaxCents = ledgerOutputStandardCents + ledgerOutputReducedCents + ledgerOutputGenericCents;
246
+ const outputTaxDifferenceCents = ledgerOutputTaxCents - formDomesticTaxCents;
247
+ checks.push(`domestic output tax: ledger ${ledgerOutputTaxCents} vs form ${formDomesticTaxCents} (diff ${outputTaxDifferenceCents})`);
248
+ if (Math.abs(outputTaxDifferenceCents) > tolerance) {
249
+ issues.push(`domestic output tax on the ledger differs from the form computation by ${outputTaxDifferenceCents} cents (tolerance ${tolerance}) — check bases and VAT postings`);
250
+ }
251
+ const igeDeductionDiff = kzValue(61) - ledgerIgeOutputCents;
252
+ if (kzValue(61) !== 0 || ledgerIgeOutputCents !== 0) {
253
+ checks.push(`igE input vs output tax: Kz 61 ${kzValue(61)} vs ledger igE output ${ledgerIgeOutputCents} (diff ${igeDeductionDiff}; non-zero = partial deduction or inconsistency)`);
254
+ }
255
+ const rcDeductionDiff = kzValue(67) - (kz47Cents + kz85Cents);
256
+ if (kzValue(67) !== 0 || kz47Cents !== 0 || kz85Cents !== 0) {
257
+ checks.push(`§13b input vs output tax: Kz 67 ${kzValue(67)} vs Kz 47+85 ${kz47Cents + kz85Cents} (diff ${rcDeductionDiff}; non-zero = partial deduction or inconsistency)`);
258
+ }
259
+ const kennzahlen = KENNZAHL_ORDER.filter((kennzahl) => values.has(kennzahl)).map((kennzahl) => values.get(kennzahl));
260
+ return {
261
+ chart: input.chart,
262
+ year: input.year,
263
+ period: input.period,
264
+ taxationMethod: input.taxationMethod ?? 'soll',
265
+ kennzahlen,
266
+ zahllastCents,
267
+ reconciliation: {
268
+ ledgerOutputTaxCents,
269
+ formOutputTaxCents: formDomesticTaxCents,
270
+ outputTaxDifferenceCents,
271
+ checks,
272
+ },
273
+ issues,
274
+ };
275
+ }
276
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXN0dmEuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy91c3R2YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW1CRztBQUNILE9BQU8sRUFBRSxlQUFlLEVBQWUsTUFBTSxxQkFBcUIsQ0FBQztBQUNuRSxPQUFPLEVBQUUsUUFBUSxFQUFzQixNQUFNLG1CQUFtQixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQVNuRCxNQUFNLGVBQWUsR0FBMkI7SUFDOUMsRUFBRSxFQUFFLCtCQUErQjtJQUNuQyxFQUFFLEVBQUUsOEJBQThCO0lBQ2xDLEVBQUUsRUFBRSxxQ0FBcUM7SUFDekMsRUFBRSxFQUFFLG9DQUFvQztJQUN4QyxFQUFFLEVBQUUsZ0RBQWdEO0lBQ3BELEVBQUUsRUFBRSx3REFBd0Q7SUFDNUQsRUFBRSxFQUFFLHlDQUF5QztJQUM3QyxFQUFFLEVBQUUsZ0VBQWdFO0lBQ3BFLEVBQUUsRUFBRSx1QkFBdUI7SUFDM0IsRUFBRSxFQUFFLG1FQUFtRTtJQUN2RSxFQUFFLEVBQUUsdUJBQXVCO0lBQzNCLEVBQUUsRUFBRSxxRkFBcUY7SUFDekYsRUFBRSxFQUFFLDhDQUE4QztJQUNsRCxFQUFFLEVBQUUsOENBQThDO0lBQ2xELEVBQUUsRUFBRSxpQ0FBaUM7SUFDckMsRUFBRSxFQUFFLGlEQUFpRDtJQUNyRCxFQUFFLEVBQUUsNkRBQTZEO0lBQ2pFLEVBQUUsRUFBRSxzREFBc0Q7Q0FDM0QsQ0FBQztBQUVGLDZDQUE2QztBQUM3QyxNQUFNLGNBQWMsR0FBRyxDQUFDLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFB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QUFDLFFBQVEsQ0FBQyxJQUFJLFlBQVksUUFBUSxFQUFFO1lBQzFELFFBQVE7U0FDVCxDQUFDLENBQUM7SUFDTCxDQUFDLENBQUM7SUFFRixLQUFLLE1BQU0sQ0FBQyxRQUFRLEVBQUUsS0FBSyxDQUFDLElBQUksYUFBYSxFQUFFLENBQUM7UUFDOUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsYUFBYSxDQUFDLEtBQUssQ0FBQyxFQUFFLFlBQVksQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7SUFDbkYsQ0FBQztJQUNELEtBQUssTUFBTSxDQUFDLFFBQVEsRUFBRSxLQUFLLENBQUMsSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUM3QyxHQUFHLENBQUMsUUFBUSxFQUFFLFVBQVUsRUFBRSxLQUFLLEVBQUUsWUFBWSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBQ0QsSUFBSSxTQUFTLEtBQUssQ0FBQztRQUFFLEdBQUcsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLGFBQWEsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUM7SUFDaEYsSUFBSSxTQUFTLEtBQUssQ0FBQztRQUFFLEdBQUcsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLGFBQWEsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFDbEYsSUFBSSxTQUFTLEtBQUssQ0FBQztRQUFFLEdBQUcsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLGFBQWEsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUM7SUFDaEYsSUFBSSxTQUFTLEtBQUssQ0FBQztRQUFFLEdBQUcsQ0FBQyxFQUFFLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxZQUFZLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLG1CQUFtQixDQUFDLENBQUM7SUFDbkcsSUFBSSxTQUFTLEtBQUss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
@@ -0,0 +1,101 @@
1
+ import type { TCents } from '@fin.cx/calculation';
2
+ import type { TChartName } from '@fin.cx/chartdata';
3
+ /** period movement of one account: debitCents - creditCents */
4
+ export interface IAccountMovementInput {
5
+ accountNumber: string;
6
+ signedCents: TCents;
7
+ }
8
+ /**
9
+ * Exact Bemessungsgrundlagen per tax scenario, aggregated from
10
+ * taxScenario-tagged journal entries. Preferred over derivation from the
11
+ * VAT account movements (which only reconstructs the base by dividing the
12
+ * tax through the rate).
13
+ */
14
+ export interface IScenarioBaseInput {
15
+ /** igE 19 % net bases -> Kz 89 */
16
+ intraEuAcquisitionStandardCents?: TCents;
17
+ /** igE 7 % net bases -> Kz 93 */
18
+ intraEuAcquisitionReducedCents?: TCents;
19
+ /** §13b sonstige Leistungen from the EU -> Kz 46/47 */
20
+ reverseChargeServicesEuCents?: TCents;
21
+ /** other §13b inputs -> Kz 84/85 */
22
+ reverseChargeOtherCents?: TCents;
23
+ }
24
+ /**
25
+ * Ledger VAT accounts the module needs beyond the chartdata Kennzahl
26
+ * mappings: output tax accounts (they carry no Kennzahl of their own — the
27
+ * form computes output tax from the bases) used for reconciliation and for
28
+ * deriving igE/§13b bases when no scenario bases are supplied.
29
+ */
30
+ export interface IVatAccountSet {
31
+ /** Umsatzsteuer 19 % */
32
+ outputStandard: string[];
33
+ /** Umsatzsteuer 7 % */
34
+ outputReduced: string[];
35
+ /** undifferenzierte Umsatzsteuer — non-zero movements are flagged */
36
+ outputGeneric: string[];
37
+ /** Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 % */
38
+ outputIntraEuAcquisition: string[];
39
+ /** Umsatzsteuer nach §13b UStG 19 % */
40
+ outputReverseCharge: string[];
41
+ }
42
+ export interface IUstvaInput {
43
+ chart: TChartName;
44
+ /** chart dataset year (and the year the period belongs to) */
45
+ year: number;
46
+ /** filing period label, informational: '2026-09', '2026-Q3', '2026' */
47
+ period: string;
48
+ /** account movements of the period (debitCents - creditCents) */
49
+ movements: IAccountMovementInput[];
50
+ scenarioBases?: IScenarioBaseInput;
51
+ /**
52
+ * Versteuerungsart (§16 vs §20 UStG). This module aggregates whatever
53
+ * movements the caller selected: Soll = by invoice date, Ist = by payment
54
+ * receipt. The value is recorded on the result, not interpreted.
55
+ */
56
+ taxationMethod?: 'soll' | 'ist';
57
+ /** Kz 39: Sondervorauszahlung to credit in the final period (Dauerfrist) */
58
+ sondervorauszahlungCents?: TCents;
59
+ /** revenue accounts intentionally outside the UStVA (no issue raised) */
60
+ exemptAccounts?: string[];
61
+ /** override the built-in per-chart VAT account table */
62
+ vatAccounts?: Partial<IVatAccountSet>;
63
+ /**
64
+ * tolerance for the output-tax reconciliation (ledger vs form), in cents.
65
+ * Truncated bases and per-invoice rounding cause small legitimate drift.
66
+ * Default 500 (5.00 EUR).
67
+ */
68
+ reconciliationToleranceCents?: TCents;
69
+ }
70
+ export type TKennzahlKind = 'baseEur' | 'taxCents';
71
+ export interface IKennzahlValue {
72
+ kennzahl: number;
73
+ kind: TKennzahlKind;
74
+ /** baseEur: full euros, cents truncated toward zero; taxCents: exact cents */
75
+ value: number;
76
+ label: string;
77
+ /** contributing ledger accounts ('(scenario)'/'(derived)' for non-account sources) */
78
+ accounts: string[];
79
+ }
80
+ export interface IUstvaReconciliation {
81
+ /** output tax movement on the ledger VAT accounts (std + reduced + generic) */
82
+ ledgerOutputTaxCents: TCents;
83
+ /** output tax the form computes from the truncated domestic bases */
84
+ formOutputTaxCents: TCents;
85
+ outputTaxDifferenceCents: TCents;
86
+ /** human-readable per-check lines (always populated, also when green) */
87
+ checks: string[];
88
+ }
89
+ export interface IUstvaResult {
90
+ chart: TChartName;
91
+ year: number;
92
+ period: string;
93
+ taxationMethod: 'soll' | 'ist';
94
+ /** in form order; zero-valued Kennzahlen without contributions omitted */
95
+ kennzahlen: IKennzahlValue[];
96
+ /** Kz 83: positive = Zahllast, negative = Erstattung */
97
+ zahllastCents: TCents;
98
+ reconciliation: IUstvaReconciliation;
99
+ /** hard problems — a filing must not be generated while non-empty */
100
+ issues: string[];
101
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXN0dmEudHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy91c3R2YS50eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
package/license ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Task Venture Capital GmbH
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@fin.cx/tax",
3
+ "version": "1.1.0",
4
+ "private": false,
5
+ "description": "German tax computation engine: UStVA Kennzahl aggregation from ledger balances with form rounding, Zahllast and reconciliation checks — exact cents",
6
+ "main": "dist_ts/index.js",
7
+ "typings": "dist_ts/index.d.ts",
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://code.foss.global/fin.cx/tax.git"
12
+ },
13
+ "keywords": [
14
+ "ustva",
15
+ "umsatzsteuer",
16
+ "vat",
17
+ "elster",
18
+ "kennzahlen",
19
+ "tax",
20
+ "accounting",
21
+ "german",
22
+ "typescript"
23
+ ],
24
+ "author": "fin.cx",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://code.foss.global/fin.cx/tax/issues"
28
+ },
29
+ "homepage": "https://code.foss.global/fin.cx/tax#readme",
30
+ "dependencies": {
31
+ "@fin.cx/calculation": "^1.1.1",
32
+ "@fin.cx/chartdata": "^2.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@git.zone/tsbuild": "^4.4.2",
36
+ "@git.zone/tsrun": "^2.0.5",
37
+ "@git.zone/tstest": "^3.6.6",
38
+ "@types/node": "^26.1.1"
39
+ },
40
+ "files": [
41
+ "ts/**/*",
42
+ "dist_ts/**/*",
43
+ "dist_ts_web/**/*",
44
+ "readme.md",
45
+ "license"
46
+ ],
47
+ "scripts": {
48
+ "test": "(tstest test/ --verbose)",
49
+ "build": "(tsbuild --web --node)",
50
+ "buildDocs": "tsdoc"
51
+ }
52
+ }
package/readme.md ADDED
@@ -0,0 +1,65 @@
1
+ # @fin.cx/tax
2
+
3
+ German tax computation engine. The first module computes the **UStVA (Umsatzsteuer-Voranmeldung) Kennzahlen** from ledger account movements — pure functions, exact integer cents, loud failures.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm install @fin.cx/tax
9
+ ```
10
+
11
+ ## What it does
12
+
13
+ `computeUstva()` turns one period's account movements into the UStVA figures:
14
+
15
+ - **Account→Kennzahl mapping** comes from [@fin.cx/chartdata](https://code.foss.global/fin.cx/chartdata) (SKR03/SKR04, year-versioned). Revenue accounts feed the Bemessungsgrundlagen (Kz 81, 86, 41, 43, 48, 60); Vorsteuer accounts feed the tax fields (Kz 66, 61, 67, 62) cent-exact.
16
+ - **Form rounding is applied correctly**: Bemessungsgrundlagen are declared in full euros with cents truncated toward zero — on the *period sum per Kennzahl*, not per booking. Tax amounts stay cent-exact. Output tax on domestic and igE bases is what the form computes from the truncated bases (`base EUR × 19` is an exact integer number of cents).
17
+ - **igE and inbound §13b bases** (Kz 89/93, 46/47, 84/85) are scenario figures, not single-account figures. Pass them exactly via `scenarioBases` (aggregated from tax-scenario-tagged journal entries, e.g. @fin.cx/skr's `taxScenario`); without them the module derives the 19 % bases from the output-tax account movements and marks them `(derived)`.
18
+ - **Kz 83 (Zahllast/Erstattung)** is computed the way the form computes it, including an optional Sondervorauszahlung credit (Kz 39, Dauerfristverlängerung).
19
+ - **Reconciliation**: the ledger's output-tax balances are compared against the form computation; small drift from base truncation and per-invoice rounding is reported, drift beyond the tolerance (default 5.00 EUR) becomes an issue. igE and §13b input/output tax symmetry is checked too.
20
+ - **Loud failures**: revenue accounts without a UStVA mapping, unknown revenue-class accounts, and undifferentiated output-VAT balances land in `issues`. **Do not generate a filing while `issues` is non-empty.**
21
+
22
+ ## Usage
23
+
24
+ ```typescript
25
+ import { computeUstva } from '@fin.cx/tax';
26
+
27
+ const result = computeUstva({
28
+ chart: 'SKR03',
29
+ year: 2026,
30
+ period: '2026-09',
31
+ // debitCents - creditCents per account, from your trial balance
32
+ movements: [
33
+ { accountNumber: '8400', signedCents: -1_000_055 }, // 10 000.55 revenue 19 %
34
+ { accountNumber: '1776', signedCents: -190_010 }, // output VAT ledger balance
35
+ { accountNumber: '1576', signedCents: 123_456 }, // input VAT
36
+ ],
37
+ // exact igE / §13b bases from tax-scenario-tagged entries (optional)
38
+ scenarioBases: { intraEuAcquisitionStandardCents: 200_000 },
39
+ });
40
+
41
+ if (result.issues.length > 0) throw new Error(result.issues.join('\n'));
42
+ for (const kz of result.kennzahlen) {
43
+ // kind 'baseEur' -> full euros; kind 'taxCents' -> cents
44
+ console.log(`Kz ${kz.kennzahl}: ${kz.value} (${kz.label})`);
45
+ }
46
+ console.log('Zahllast:', result.zahllastCents, 'cents');
47
+ console.log(result.reconciliation.checks.join('\n'));
48
+ ```
49
+
50
+ ## Soll- vs. Ist-Versteuerung (§16 / §20 UStG)
51
+
52
+ This module aggregates whatever movements you pass in — it does not decide *which* entries belong to the period:
53
+
54
+ - **Soll-Versteuerung** (default assumption): pass movements selected by invoice/posting date.
55
+ - **Ist-Versteuerung**: pass movements selected by payment receipt.
56
+
57
+ Set `taxationMethod` so the result records which mode the figures represent. If you are unsure which regime applies to your company, resolve that before filing — it materially changes the period selection.
58
+
59
+ ## Scope
60
+
61
+ Transport to ELSTER is deliberately out of scope — this package computes figures; `@fin.cx/elster` (ERiC bridge) files them. KSt/GewSt/year-end tax computation will land here as further modules.
62
+
63
+ ## License
64
+
65
+ MIT — see [license](./license).
@@ -0,0 +1,8 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export const commitinfo = {
5
+ name: '@fin.cx/tax',
6
+ version: '1.1.0',
7
+ description: 'German tax computation engine: UStVA Kennzahl aggregation from ledger balances with form rounding, Zahllast and reconciliation checks — exact cents'
8
+ }
package/ts/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './ustva.types.js';
2
+ export * from './ustva.accounts.js';
3
+ export * from './ustva.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Output-VAT ledger accounts per chart. These accounts carry no UStVA
3
+ * Kennzahl in @fin.cx/chartdata (output tax is computed by the form from
4
+ * the bases); the tax module needs them for reconciliation and for deriving
5
+ * igE/§13b bases when no scenario bases are supplied.
6
+ *
7
+ * Numbers match the @fin.cx/skr account policies (SKR03/SKR04 defaults).
8
+ */
9
+ import type { TChartName } from '@fin.cx/chartdata';
10
+ import type { IVatAccountSet } from './ustva.types.js';
11
+
12
+ export const VAT_ACCOUNTS: Record<TChartName, IVatAccountSet> = {
13
+ SKR03: {
14
+ outputStandard: ['1776'],
15
+ outputReduced: ['1771'],
16
+ outputGeneric: ['1770'],
17
+ outputIntraEuAcquisition: ['1784'],
18
+ outputReverseCharge: ['1787'],
19
+ },
20
+ SKR04: {
21
+ outputStandard: ['3806'],
22
+ outputReduced: ['3801'],
23
+ outputGeneric: ['3800'],
24
+ outputIntraEuAcquisition: ['3804'],
25
+ outputReverseCharge: ['3837'],
26
+ },
27
+ };
package/ts/ustva.ts ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * UStVA Kennzahl computation — pure, ledger-agnostic.
3
+ *
4
+ * Input: period account movements in exact cents (debit - credit, as
5
+ * @fin.cx/skr's trial-balance aggregation delivers), plus optional exact
6
+ * scenario bases from taxScenario-tagged entries. Mapping authority is
7
+ * @fin.cx/chartdata (account→Kennzahl); this module adds the form
8
+ * arithmetic:
9
+ *
10
+ * - Bemessungsgrundlagen are declared in FULL EUROS, cents truncated toward
11
+ * zero — truncation applies to the period sum per Kennzahl, not per booking.
12
+ * - Tax amounts (Vorsteuer, §13b tax) are cent-exact.
13
+ * - Output tax on domestic/igE bases is what the form computes from the
14
+ * truncated bases (base EUR × 19 = cents, exactly integer), NOT the ledger
15
+ * balance; the difference is surfaced in the reconciliation.
16
+ * - Kz 83 (Zahllast) is computed the way the form computes it.
17
+ *
18
+ * Unattributable movements FAIL LOUDLY via `issues` — a filing must not be
19
+ * generated while issues is non-empty.
20
+ */
21
+ import { vatFromNetCents, type TCents } from '@fin.cx/calculation';
22
+ import { getChart, type IChartAccount } from '@fin.cx/chartdata';
23
+ import { VAT_ACCOUNTS } from './ustva.accounts.js';
24
+ import type {
25
+ IKennzahlValue,
26
+ IUstvaInput,
27
+ IUstvaResult,
28
+ IVatAccountSet,
29
+ TKennzahlKind,
30
+ } from './ustva.types.js';
31
+
32
+ const KENNZAHL_LABELS: Record<number, string> = {
33
+ 81: 'Steuerpflichtige Umsätze 19 %',
34
+ 86: 'Steuerpflichtige Umsätze 7 %',
35
+ 89: 'Innergemeinschaftliche Erwerbe 19 %',
36
+ 93: 'Innergemeinschaftliche Erwerbe 7 %',
37
+ 41: 'Steuerfreie innergemeinschaftliche Lieferungen',
38
+ 43: 'Steuerfreie Umsätze mit Vorsteuerabzug (u. a. Ausfuhr)',
39
+ 48: 'Steuerfreie Umsätze ohne Vorsteuerabzug',
40
+ 46: 'Sonstige Leistungen aus dem übrigen Gemeinschaftsgebiet (§13b)',
41
+ 47: 'Steuer zu Kennzahl 46',
42
+ 84: 'Andere Leistungen eines im Ausland ansässigen Unternehmers (§13b)',
43
+ 85: 'Steuer zu Kennzahl 84',
44
+ 60: 'Steuerpflichtige Umsätze, für die der Leistungsempfänger die Steuer schuldet (§13b)',
45
+ 66: 'Vorsteuer aus Rechnungen anderer Unternehmer',
46
+ 61: 'Vorsteuer aus innergemeinschaftlichem Erwerb',
47
+ 62: 'Entrichtete Einfuhrumsatzsteuer',
48
+ 67: 'Vorsteuer aus Leistungen im Sinne des §13b UStG',
49
+ 39: 'Anrechnung der Sondervorauszahlung (Dauerfristverlängerung)',
50
+ 83: 'Verbleibende Umsatzsteuer-Vorauszahlung / Überschuss',
51
+ };
52
+
53
+ /** result order follows the form sections */
54
+ const KENNZAHL_ORDER = [81, 86, 89, 93, 41, 43, 48, 46, 47, 84, 85, 60, 66, 61, 62, 67, 39, 83];
55
+
56
+ /** full euros, cents truncated toward zero (form rule for bases) */
57
+ function truncateToEur(cents: TCents): number {
58
+ return Math.trunc(cents / 100);
59
+ }
60
+
61
+ /** cents of `ratePercent` on a full-euro base — exact integer arithmetic */
62
+ function taxOnEurBase(baseEur: number, ratePercent: number): TCents {
63
+ return baseEur * ratePercent;
64
+ }
65
+
66
+ function isRevenueClassNumber(chart: 'SKR03' | 'SKR04', accountNumber: string): boolean {
67
+ const firstDigit = accountNumber[0];
68
+ return chart === 'SKR03' ? firstDigit === '8' : firstDigit === '4';
69
+ }
70
+
71
+ export function computeUstva(input: IUstvaInput): IUstvaResult {
72
+ const dataset = getChart(input.chart, input.year);
73
+ const byNumber = new Map(dataset.accounts.map((account) => [account.accountNumber, account]));
74
+ const vatAccounts: IVatAccountSet = { ...VAT_ACCOUNTS[input.chart], ...input.vatAccounts };
75
+ const exempt = new Set(input.exemptAccounts ?? []);
76
+ const tolerance = input.reconciliationToleranceCents ?? 500;
77
+ const issues: string[] = [];
78
+ const checks: string[] = [];
79
+
80
+ // ---- pass 1: bucket the movements -------------------------------------
81
+ const baseCentsByKz = new Map<number, TCents>();
82
+ const taxCentsByKz = new Map<number, TCents>();
83
+ const accountsByKz = new Map<number, string[]>();
84
+ const contribute = (
85
+ bucket: Map<number, TCents>,
86
+ kennzahl: number,
87
+ cents: TCents,
88
+ accountNumber: string,
89
+ ): void => {
90
+ bucket.set(kennzahl, (bucket.get(kennzahl) ?? 0) + cents);
91
+ const list = accountsByKz.get(kennzahl) ?? [];
92
+ if (!list.includes(accountNumber)) list.push(accountNumber);
93
+ accountsByKz.set(kennzahl, list);
94
+ };
95
+
96
+ let ledgerOutputStandardCents = 0;
97
+ let ledgerOutputReducedCents = 0;
98
+ let ledgerOutputGenericCents = 0;
99
+ let ledgerIgeOutputCents = 0;
100
+ let ledgerRcOutputCents = 0;
101
+ const unknownAccounts: string[] = [];
102
+
103
+ for (const movement of input.movements) {
104
+ if (movement.signedCents === 0) continue;
105
+ const { accountNumber, signedCents } = movement;
106
+
107
+ // output tax accounts (no Kennzahl of their own)
108
+ if (vatAccounts.outputStandard.includes(accountNumber)) {
109
+ ledgerOutputStandardCents += -signedCents;
110
+ continue;
111
+ }
112
+ if (vatAccounts.outputReduced.includes(accountNumber)) {
113
+ ledgerOutputReducedCents += -signedCents;
114
+ continue;
115
+ }
116
+ if (vatAccounts.outputGeneric.includes(accountNumber)) {
117
+ ledgerOutputGenericCents += -signedCents;
118
+ continue;
119
+ }
120
+ if (vatAccounts.outputIntraEuAcquisition.includes(accountNumber)) {
121
+ ledgerIgeOutputCents += -signedCents;
122
+ continue;
123
+ }
124
+ if (vatAccounts.outputReverseCharge.includes(accountNumber)) {
125
+ ledgerRcOutputCents += -signedCents;
126
+ continue;
127
+ }
128
+
129
+ const account = byNumber.get(accountNumber);
130
+ if (!account) {
131
+ const numeric = Number.parseInt(accountNumber, 10);
132
+ if (numeric >= 10000 && numeric <= 99999) continue; // personal accounts
133
+ if (isRevenueClassNumber(input.chart, accountNumber) && !exempt.has(accountNumber)) {
134
+ issues.push(
135
+ `revenue-class account ${accountNumber} is unknown in ${input.chart}/${input.year} — cannot attribute its movement to a Kennzahl`,
136
+ );
137
+ } else {
138
+ unknownAccounts.push(accountNumber);
139
+ }
140
+ continue;
141
+ }
142
+
143
+ if (account.ustva) {
144
+ if (account.ustva.role === 'base') {
145
+ // revenue bases carry credit balances -> base = credit surplus
146
+ contribute(baseCentsByKz, account.ustva.kennzahl, -signedCents, accountNumber);
147
+ } else {
148
+ // Vorsteuer accounts are assets (debit balances)
149
+ const sign = account.accountType === 'asset' || account.accountType === 'expense' ? 1 : -1;
150
+ contribute(taxCentsByKz, account.ustva.kennzahl, sign * signedCents, accountNumber);
151
+ }
152
+ continue;
153
+ }
154
+
155
+ if (account.accountType === 'revenue' && !exempt.has(accountNumber)) {
156
+ issues.push(
157
+ `revenue account ${accountNumber} (${account.accountName}) has a non-zero movement but no UStVA mapping — declare it exempt or map it`,
158
+ );
159
+ }
160
+ }
161
+
162
+ if (unknownAccounts.length > 0) {
163
+ checks.push(`accounts unknown to the chart, ignored for UStVA: ${unknownAccounts.join(', ')}`);
164
+ }
165
+ if (ledgerOutputGenericCents !== 0) {
166
+ issues.push(
167
+ `undifferentiated output VAT account carries ${ledgerOutputGenericCents} cents — cannot be attributed to a rate; rebook to the 19 %/7 % accounts`,
168
+ );
169
+ }
170
+
171
+ // ---- scenario bases: igE and inbound §13b ------------------------------
172
+ const scenario = input.scenarioBases ?? {};
173
+ const derived: string[] = [];
174
+
175
+ let kz89Cents = scenario.intraEuAcquisitionStandardCents ?? 0;
176
+ let kz89Source = '(scenario)';
177
+ if (scenario.intraEuAcquisitionStandardCents === undefined && ledgerIgeOutputCents !== 0) {
178
+ kz89Cents = Math.round((ledgerIgeOutputCents * 100) / 19);
179
+ kz89Source = '(derived)';
180
+ derived.push(`Kz 89 derived from igE output tax movement (${ledgerIgeOutputCents} cents / 19 %)`);
181
+ }
182
+ const kz93Cents = scenario.intraEuAcquisitionReducedCents ?? 0;
183
+
184
+ let kz46Cents = scenario.reverseChargeServicesEuCents ?? 0;
185
+ let kz46Source = '(scenario)';
186
+ const kz84Cents = scenario.reverseChargeOtherCents ?? 0;
187
+ const hasRcScenario =
188
+ scenario.reverseChargeServicesEuCents !== undefined ||
189
+ scenario.reverseChargeOtherCents !== undefined;
190
+ if (!hasRcScenario && ledgerRcOutputCents !== 0) {
191
+ kz46Cents = Math.round((ledgerRcOutputCents * 100) / 19);
192
+ kz46Source = '(derived)';
193
+ derived.push(
194
+ `Kz 46 derived from §13b output tax movement (${ledgerRcOutputCents} cents / 19 %) — split vs Kz 84 unknown without scenario bases`,
195
+ );
196
+ }
197
+
198
+ // §13b tax split: keep the total identical to the ledger movement
199
+ let kz47Cents = 0;
200
+ let kz85Cents = 0;
201
+ if (kz46Cents !== 0 && kz84Cents !== 0) {
202
+ kz47Cents = vatFromNetCents(kz46Cents, 19);
203
+ kz85Cents = ledgerRcOutputCents - kz47Cents;
204
+ } else if (kz84Cents !== 0) {
205
+ kz85Cents = ledgerRcOutputCents;
206
+ } else if (kz46Cents !== 0) {
207
+ kz47Cents = ledgerRcOutputCents;
208
+ } else if (ledgerRcOutputCents !== 0) {
209
+ kz47Cents = ledgerRcOutputCents; // derived path above set Kz 46
210
+ }
211
+
212
+ // consistency: scenario bases × rate must reproduce the ledger tax
213
+ if (kz89Source === '(scenario)' && kz89Cents !== 0) {
214
+ const expected = vatFromNetCents(kz89Cents, 19);
215
+ const diff = ledgerIgeOutputCents - expected;
216
+ checks.push(`igE 19 %: ledger output tax ${ledgerIgeOutputCents} vs 19 % of scenario base ${expected} (diff ${diff})`);
217
+ if (Math.abs(diff) > tolerance) {
218
+ issues.push(`igE scenario base and igE output tax ledger movement disagree by ${diff} cents`);
219
+ }
220
+ }
221
+ if (hasRcScenario) {
222
+ const expected = vatFromNetCents(kz46Cents, 19) + vatFromNetCents(kz84Cents, 19);
223
+ const diff = ledgerRcOutputCents - expected;
224
+ checks.push(`§13b: ledger output tax ${ledgerRcOutputCents} vs 19 % of scenario bases ${expected} (diff ${diff})`);
225
+ if (Math.abs(diff) > tolerance) {
226
+ issues.push(`§13b scenario bases and §13b output tax ledger movement disagree by ${diff} cents`);
227
+ }
228
+ }
229
+ checks.push(...derived);
230
+
231
+ // ---- assemble the Kennzahlen -------------------------------------------
232
+ const values = new Map<number, IKennzahlValue>();
233
+ const put = (kennzahl: number, kind: TKennzahlKind, value: number, accounts: string[]): void => {
234
+ if (value === 0 && accounts.length === 0) return;
235
+ values.set(kennzahl, {
236
+ kennzahl,
237
+ kind,
238
+ value,
239
+ label: KENNZAHL_LABELS[kennzahl] ?? `Kennzahl ${kennzahl}`,
240
+ accounts,
241
+ });
242
+ };
243
+
244
+ for (const [kennzahl, cents] of baseCentsByKz) {
245
+ put(kennzahl, 'baseEur', truncateToEur(cents), accountsByKz.get(kennzahl) ?? []);
246
+ }
247
+ for (const [kennzahl, cents] of taxCentsByKz) {
248
+ put(kennzahl, 'taxCents', cents, accountsByKz.get(kennzahl) ?? []);
249
+ }
250
+ if (kz89Cents !== 0) put(89, 'baseEur', truncateToEur(kz89Cents), [kz89Source]);
251
+ if (kz93Cents !== 0) put(93, 'baseEur', truncateToEur(kz93Cents), ['(scenario)']);
252
+ if (kz46Cents !== 0) put(46, 'baseEur', truncateToEur(kz46Cents), [kz46Source]);
253
+ if (kz47Cents !== 0) put(47, 'taxCents', kz47Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
254
+ if (kz84Cents !== 0) put(84, 'baseEur', truncateToEur(kz84Cents), ['(scenario)']);
255
+ if (kz85Cents !== 0) put(85, 'taxCents', kz85Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
256
+ const sondervorauszahlung = input.sondervorauszahlungCents ?? 0;
257
+ if (sondervorauszahlung !== 0) put(39, 'taxCents', sondervorauszahlung, ['(input)']);
258
+
259
+ const kzValue = (kennzahl: number): number => values.get(kennzahl)?.value ?? 0;
260
+
261
+ // ---- form arithmetic ----------------------------------------------------
262
+ const formDomesticTaxCents =
263
+ taxOnEurBase(kzValue(81), 19) + taxOnEurBase(kzValue(86), 7);
264
+ const formIgeTaxCents = taxOnEurBase(kzValue(89), 19) + taxOnEurBase(kzValue(93), 7);
265
+ const inputTaxCents = kzValue(66) + kzValue(61) + kzValue(67) + kzValue(62);
266
+ const zahllastCents =
267
+ formDomesticTaxCents +
268
+ formIgeTaxCents +
269
+ kz47Cents +
270
+ kz85Cents -
271
+ inputTaxCents -
272
+ sondervorauszahlung;
273
+ put(83, 'taxCents', zahllastCents, ['(computed)']);
274
+
275
+ // ---- reconciliation ------------------------------------------------------
276
+ const ledgerOutputTaxCents =
277
+ ledgerOutputStandardCents + ledgerOutputReducedCents + ledgerOutputGenericCents;
278
+ const outputTaxDifferenceCents = ledgerOutputTaxCents - formDomesticTaxCents;
279
+ checks.push(
280
+ `domestic output tax: ledger ${ledgerOutputTaxCents} vs form ${formDomesticTaxCents} (diff ${outputTaxDifferenceCents})`,
281
+ );
282
+ if (Math.abs(outputTaxDifferenceCents) > tolerance) {
283
+ issues.push(
284
+ `domestic output tax on the ledger differs from the form computation by ${outputTaxDifferenceCents} cents (tolerance ${tolerance}) — check bases and VAT postings`,
285
+ );
286
+ }
287
+ const igeDeductionDiff = kzValue(61) - ledgerIgeOutputCents;
288
+ if (kzValue(61) !== 0 || ledgerIgeOutputCents !== 0) {
289
+ checks.push(
290
+ `igE input vs output tax: Kz 61 ${kzValue(61)} vs ledger igE output ${ledgerIgeOutputCents} (diff ${igeDeductionDiff}; non-zero = partial deduction or inconsistency)`,
291
+ );
292
+ }
293
+ const rcDeductionDiff = kzValue(67) - (kz47Cents + kz85Cents);
294
+ if (kzValue(67) !== 0 || kz47Cents !== 0 || kz85Cents !== 0) {
295
+ checks.push(
296
+ `§13b input vs output tax: Kz 67 ${kzValue(67)} vs Kz 47+85 ${kz47Cents + kz85Cents} (diff ${rcDeductionDiff}; non-zero = partial deduction or inconsistency)`,
297
+ );
298
+ }
299
+
300
+ const kennzahlen = KENNZAHL_ORDER.filter((kennzahl) => values.has(kennzahl)).map(
301
+ (kennzahl) => values.get(kennzahl)!,
302
+ );
303
+
304
+ return {
305
+ chart: input.chart,
306
+ year: input.year,
307
+ period: input.period,
308
+ taxationMethod: input.taxationMethod ?? 'soll',
309
+ kennzahlen,
310
+ zahllastCents,
311
+ reconciliation: {
312
+ ledgerOutputTaxCents,
313
+ formOutputTaxCents: formDomesticTaxCents,
314
+ outputTaxDifferenceCents,
315
+ checks,
316
+ },
317
+ issues,
318
+ };
319
+ }
@@ -0,0 +1,109 @@
1
+ import type { TCents } from '@fin.cx/calculation';
2
+ import type { TChartName } from '@fin.cx/chartdata';
3
+
4
+ /** period movement of one account: debitCents - creditCents */
5
+ export interface IAccountMovementInput {
6
+ accountNumber: string;
7
+ signedCents: TCents;
8
+ }
9
+
10
+ /**
11
+ * Exact Bemessungsgrundlagen per tax scenario, aggregated from
12
+ * taxScenario-tagged journal entries. Preferred over derivation from the
13
+ * VAT account movements (which only reconstructs the base by dividing the
14
+ * tax through the rate).
15
+ */
16
+ export interface IScenarioBaseInput {
17
+ /** igE 19 % net bases -> Kz 89 */
18
+ intraEuAcquisitionStandardCents?: TCents;
19
+ /** igE 7 % net bases -> Kz 93 */
20
+ intraEuAcquisitionReducedCents?: TCents;
21
+ /** §13b sonstige Leistungen from the EU -> Kz 46/47 */
22
+ reverseChargeServicesEuCents?: TCents;
23
+ /** other §13b inputs -> Kz 84/85 */
24
+ reverseChargeOtherCents?: TCents;
25
+ }
26
+
27
+ /**
28
+ * Ledger VAT accounts the module needs beyond the chartdata Kennzahl
29
+ * mappings: output tax accounts (they carry no Kennzahl of their own — the
30
+ * form computes output tax from the bases) used for reconciliation and for
31
+ * deriving igE/§13b bases when no scenario bases are supplied.
32
+ */
33
+ export interface IVatAccountSet {
34
+ /** Umsatzsteuer 19 % */
35
+ outputStandard: string[];
36
+ /** Umsatzsteuer 7 % */
37
+ outputReduced: string[];
38
+ /** undifferenzierte Umsatzsteuer — non-zero movements are flagged */
39
+ outputGeneric: string[];
40
+ /** Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 % */
41
+ outputIntraEuAcquisition: string[];
42
+ /** Umsatzsteuer nach §13b UStG 19 % */
43
+ outputReverseCharge: string[];
44
+ }
45
+
46
+ export interface IUstvaInput {
47
+ chart: TChartName;
48
+ /** chart dataset year (and the year the period belongs to) */
49
+ year: number;
50
+ /** filing period label, informational: '2026-09', '2026-Q3', '2026' */
51
+ period: string;
52
+ /** account movements of the period (debitCents - creditCents) */
53
+ movements: IAccountMovementInput[];
54
+ scenarioBases?: IScenarioBaseInput;
55
+ /**
56
+ * Versteuerungsart (§16 vs §20 UStG). This module aggregates whatever
57
+ * movements the caller selected: Soll = by invoice date, Ist = by payment
58
+ * receipt. The value is recorded on the result, not interpreted.
59
+ */
60
+ taxationMethod?: 'soll' | 'ist';
61
+ /** Kz 39: Sondervorauszahlung to credit in the final period (Dauerfrist) */
62
+ sondervorauszahlungCents?: TCents;
63
+ /** revenue accounts intentionally outside the UStVA (no issue raised) */
64
+ exemptAccounts?: string[];
65
+ /** override the built-in per-chart VAT account table */
66
+ vatAccounts?: Partial<IVatAccountSet>;
67
+ /**
68
+ * tolerance for the output-tax reconciliation (ledger vs form), in cents.
69
+ * Truncated bases and per-invoice rounding cause small legitimate drift.
70
+ * Default 500 (5.00 EUR).
71
+ */
72
+ reconciliationToleranceCents?: TCents;
73
+ }
74
+
75
+ export type TKennzahlKind = 'baseEur' | 'taxCents';
76
+
77
+ export interface IKennzahlValue {
78
+ kennzahl: number;
79
+ kind: TKennzahlKind;
80
+ /** baseEur: full euros, cents truncated toward zero; taxCents: exact cents */
81
+ value: number;
82
+ label: string;
83
+ /** contributing ledger accounts ('(scenario)'/'(derived)' for non-account sources) */
84
+ accounts: string[];
85
+ }
86
+
87
+ export interface IUstvaReconciliation {
88
+ /** output tax movement on the ledger VAT accounts (std + reduced + generic) */
89
+ ledgerOutputTaxCents: TCents;
90
+ /** output tax the form computes from the truncated domestic bases */
91
+ formOutputTaxCents: TCents;
92
+ outputTaxDifferenceCents: TCents;
93
+ /** human-readable per-check lines (always populated, also when green) */
94
+ checks: string[];
95
+ }
96
+
97
+ export interface IUstvaResult {
98
+ chart: TChartName;
99
+ year: number;
100
+ period: string;
101
+ taxationMethod: 'soll' | 'ist';
102
+ /** in form order; zero-valued Kennzahlen without contributions omitted */
103
+ kennzahlen: IKennzahlValue[];
104
+ /** Kz 83: positive = Zahllast, negative = Erstattung */
105
+ zahllastCents: TCents;
106
+ reconciliation: IUstvaReconciliation;
107
+ /** hard problems — a filing must not be generated while non-empty */
108
+ issues: string[];
109
+ }