@fin.cx/tax 2.0.0 → 2.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,92 @@
1
+ /**
2
+ * The Zusammenfassende Meldung (§ 18a UStG): its lines, and the CSV the
3
+ * ELSTER (or BZSt-Online-Portal) online form imports. Pure; the caller
4
+ * decides which supplies of which period go in (§ 18a Abs. 1, 2, 8 UStG) and
5
+ * transmits the form itself, authenticated, as § 18a requires.
6
+ *
7
+ * Sources, read 2026-09-25:
8
+ * - ELSTER, "Import von Meldelisten zur Zusammenfassenden Meldung"
9
+ * (helpGlobal `zmdo_import_eop`, CSV version 3.0) and the general
10
+ * "Anleitung zur Erstellung von CSV-Dateien für die Importfunktion":
11
+ * UTF-8; line 1 `#v3.0`; the form's example adds `#ve3.2.1`; one line of
12
+ * field names; then the lines, one per record "Meldezeilen (Satzart 1)":
13
+ * USt-IdNr. AN(7,14) beginning with a country code of the list; Summe
14
+ * (Euro) N(11), "eine ganze Zahl", minus allowed; Art der Leistung A(1),
15
+ * `L` innergemeinschaftliche Lieferung, `S` sonstige Leistung, `D`
16
+ * Dreiecksgeschäft. At most 1 500 lines. The example separates its lines
17
+ * with a line feed and its fields with `,`.
18
+ * - BZSt, "Zusammenfassende Meldung - Fragen und Antworten"
19
+ * (bzst.de …/ZusammenfassendeMeldung/FAQ/faq_zm.html, read 2026-09-27),
20
+ * Brexit: a Northern Irish business has a VAT ID with `XI`; the ZM states
21
+ * the intra-Community supplies, triangular trades and § 6b transfers to it,
22
+ * while services to recipients in Northern Ireland follow the rules for the
23
+ * third-country territory and are not reported (Art. 8 of the Protocol on
24
+ * Ireland/Northern Ireland covers goods only).
25
+ * - BZSt, "Anleitung zur Zusammenfassenden Meldung" (USt ZM 1/2021): one line
26
+ * per recipient and kind in a period, the bases summed (Tz. 8, 11); a later
27
+ * change of the base netted into the period of the change, possibly
28
+ * negative (Tz. 10); no "Nullmeldungen" (III.1); a correction states the
29
+ * correct totals, and `0` to storno a line (Tz. 12.2, 12.3).
30
+ */
31
+ import type { TCents } from '@fin.cx/calculation';
32
+ /** `L` innergemeinschaftliche Warenlieferung, `S` sonstige Leistung (§ 3a Abs. 2), `D` Dreiecksgeschäft (§ 25b Abs. 2) */
33
+ export type TZmKind = 'L' | 'S' | 'D';
34
+ /** One supply (or a later change of its base) of the period: the recipient's VAT ID, its kind, the base in euro cents */
35
+ export interface IZmSupplyInput {
36
+ vatId: string;
37
+ kind: TZmKind;
38
+ /** signed: a later reduction of the base is negative */
39
+ baseCents: TCents;
40
+ }
41
+ /** One line of the report: the recipient, the kind, the summed base in euro cents */
42
+ export interface IZmLine {
43
+ vatId: string;
44
+ kind: TZmKind;
45
+ baseCents: TCents;
46
+ }
47
+ export interface IZmCsv {
48
+ text: string;
49
+ /** the text as UTF-8, as the import requires */
50
+ bytes: Uint8Array;
51
+ lineCount: number;
52
+ }
53
+ /** The country codes the ZM takes (ELSTER, "Liste der Länderkennzeichen"); `GB` only for periods before 2021, so not here. */
54
+ export declare const ZM_COUNTRY_CODES: readonly string[];
55
+ export declare const ZM_MAX_LINES = 1500;
56
+ /** The CSV version lines and the field names, as the form's example gives them. */
57
+ export declare const ZM_CSV_HEAD: readonly string[];
58
+ /**
59
+ * How a sum with cents becomes the whole euros the CSV's "Summe (Euro)"
60
+ * takes. No source read states it (ELSTER: "eine ganze Zahl"; ELMA5: "in Euro
61
+ * ohne Cent"; the paper form: "volle EUR | Ct"). The cents are dropped toward
62
+ * zero, as the UStVA drops those of its bases ("in vollen Euro (ohne
63
+ * Centbeträge)", Anleitung zur Umsatzsteuer-Voranmeldung 2026), so that the
64
+ * ZM and the UStVA's Kz 41 and Kz 21, which the BZSt compares, are cut alike.
65
+ * This is a recorded decision of 2026-09-27, pending the tax advisor's
66
+ * confirmation; it is not a rule a source states.
67
+ */
68
+ export declare const ZM_EUROS_RULE = "the cents of each line's sum are dropped toward zero, as the UStVA drops those of its bases (a recorded decision of 2026-09-27, pending the tax advisor's confirmation; no source states the rule)";
69
+ /**
70
+ * The whole euros of a sum in cents, as the CSV's "Summe (Euro)" takes them:
71
+ * the cents dropped toward zero (`ZM_EUROS_RULE`), so 2 000,50 € is 2000 and
72
+ * -2 000,50 € is -2000. At most 11 digits (N(11)).
73
+ */
74
+ export declare function zmEurosOf(centsArg: TCents): number;
75
+ /**
76
+ * A recipient's VAT ID as the form takes it: upper case, without spaces, the
77
+ * country code first (`GR` written `EL`, as the list has it); 7 to 14
78
+ * characters of letters, digits, `+` and `*`; a country code of the list.
79
+ */
80
+ export declare function normalizeZmVatId(valueArg: string): string;
81
+ /** The lines of a period: one per recipient VAT ID and kind, in the order they first appear, the bases summed. */
82
+ export declare function aggregateZmLines(suppliesArg: readonly IZmSupplyInput[]): IZmLine[];
83
+ /**
84
+ * The CSV of one report. An original report leaves out a line that nets to
85
+ * zero and refuses to be written with no line left (no "Nullmeldung"); a
86
+ * correction keeps a `0` line, which stornos what was reported under it.
87
+ * Each line's sum is cut to whole euros by `zmEurosOf`. Refused beyond 1 500
88
+ * lines.
89
+ */
90
+ export declare function writeZmCsv(linesArg: readonly IZmLine[], optionsArg: {
91
+ purpose: 'original' | 'correction';
92
+ }): IZmCsv;
package/dist_ts/zm.js ADDED
@@ -0,0 +1,100 @@
1
+ /** The country codes the ZM takes (ELSTER, "Liste der Länderkennzeichen"); `GB` only for periods before 2021, so not here. */
2
+ export const ZM_COUNTRY_CODES = [
3
+ 'AT', 'BE', 'BG', 'CY', 'CZ', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'XI',
4
+ ];
5
+ export const ZM_MAX_LINES = 1500;
6
+ /** The CSV version lines and the field names, as the form's example gives them. */
7
+ export const ZM_CSV_HEAD = [
8
+ '#v3.0',
9
+ '#ve3.2.1',
10
+ 'Umsatzsteuer-Identifikationsnummer (USt-IdNr.),Summe (Euro),Art der Leistung',
11
+ ];
12
+ /**
13
+ * How a sum with cents becomes the whole euros the CSV's "Summe (Euro)"
14
+ * takes. No source read states it (ELSTER: "eine ganze Zahl"; ELMA5: "in Euro
15
+ * ohne Cent"; the paper form: "volle EUR | Ct"). The cents are dropped toward
16
+ * zero, as the UStVA drops those of its bases ("in vollen Euro (ohne
17
+ * Centbeträge)", Anleitung zur Umsatzsteuer-Voranmeldung 2026), so that the
18
+ * ZM and the UStVA's Kz 41 and Kz 21, which the BZSt compares, are cut alike.
19
+ * This is a recorded decision of 2026-09-27, pending the tax advisor's
20
+ * confirmation; it is not a rule a source states.
21
+ */
22
+ export const ZM_EUROS_RULE = 'the cents of each line\'s sum are dropped toward zero, as the UStVA drops those of its bases (a recorded decision of 2026-09-27, pending the tax advisor\'s confirmation; no source states the rule)';
23
+ /**
24
+ * The whole euros of a sum in cents, as the CSV's "Summe (Euro)" takes them:
25
+ * the cents dropped toward zero (`ZM_EUROS_RULE`), so 2 000,50 € is 2000 and
26
+ * -2 000,50 € is -2000. At most 11 digits (N(11)).
27
+ */
28
+ export function zmEurosOf(centsArg) {
29
+ if (!Number.isSafeInteger(centsArg)) {
30
+ throw new Error(`the ZM sum must be an integer number of cents, got ${centsArg}`);
31
+ }
32
+ // Math.trunc of a small negative sum is -0, which is written "0"; `+ 0` makes it a plain 0
33
+ const euros = Math.trunc(centsArg / 100) + 0;
34
+ if (String(Math.abs(euros)).length > 11) {
35
+ throw new Error(`the ZM sum has at most 11 digits (N(11)), got ${euros} euros`);
36
+ }
37
+ return euros;
38
+ }
39
+ /**
40
+ * A recipient's VAT ID as the form takes it: upper case, without spaces, the
41
+ * country code first (`GR` written `EL`, as the list has it); 7 to 14
42
+ * characters of letters, digits, `+` and `*`; a country code of the list.
43
+ */
44
+ export function normalizeZmVatId(valueArg) {
45
+ let id = String(valueArg ?? '').replace(/\s+/g, '').toUpperCase();
46
+ if (id.startsWith('GR')) {
47
+ id = `EL${id.slice(2)}`;
48
+ }
49
+ if (id.length < 7 || id.length > 14) {
50
+ throw new Error(`a ZM VAT ID has 7 to 14 characters, got '${id}'`);
51
+ }
52
+ if (!/^[A-Z]{2}[A-Z0-9+*]+$/.test(id)) {
53
+ throw new Error(`a ZM VAT ID has a country code and then letters, digits, + or *, got '${id}'`);
54
+ }
55
+ if (!ZM_COUNTRY_CODES.includes(id.slice(0, 2))) {
56
+ throw new Error(`'${id.slice(0, 2)}' is no country code the ZM takes (another member state's, or XI)`);
57
+ }
58
+ return id;
59
+ }
60
+ /** The lines of a period: one per recipient VAT ID and kind, in the order they first appear, the bases summed. */
61
+ export function aggregateZmLines(suppliesArg) {
62
+ const lines = new Map();
63
+ for (const supply of suppliesArg) {
64
+ if (supply.kind !== 'L' && supply.kind !== 'S' && supply.kind !== 'D') {
65
+ throw new Error(`a ZM kind is L, S or D, got '${String(supply.kind)}'`);
66
+ }
67
+ if (!Number.isSafeInteger(supply.baseCents)) {
68
+ throw new Error(`a ZM base must be an integer number of cents, got ${supply.baseCents}`);
69
+ }
70
+ const vatId = normalizeZmVatId(supply.vatId);
71
+ if (vatId.startsWith('XI') && supply.kind === 'S') {
72
+ throw new Error(`'${vatId}' is a recipient in Northern Ireland, which is Community territory for goods only: a service to it follows the rules for the third-country territory and is not reported in the ZM (BZSt, Zusammenfassende Meldung - Fragen und Antworten, Brexit)`);
73
+ }
74
+ const key = `${vatId}|${supply.kind}`;
75
+ const line = lines.get(key) ?? { vatId, kind: supply.kind, baseCents: 0 };
76
+ line.baseCents += supply.baseCents;
77
+ lines.set(key, line);
78
+ }
79
+ return [...lines.values()];
80
+ }
81
+ /**
82
+ * The CSV of one report. An original report leaves out a line that nets to
83
+ * zero and refuses to be written with no line left (no "Nullmeldung"); a
84
+ * correction keeps a `0` line, which stornos what was reported under it.
85
+ * Each line's sum is cut to whole euros by `zmEurosOf`. Refused beyond 1 500
86
+ * lines.
87
+ */
88
+ export function writeZmCsv(linesArg, optionsArg) {
89
+ const lines = aggregateZmLines(linesArg).filter((lineArg) => optionsArg.purpose === 'correction' || lineArg.baseCents !== 0);
90
+ if (lines.length === 0) {
91
+ throw new Error('a ZM with no line is no report: "Nullmeldungen" are not given (BZSt Anleitung III.1)');
92
+ }
93
+ if (lines.length > ZM_MAX_LINES) {
94
+ throw new Error(`the ZM online form imports at most ${ZM_MAX_LINES} lines, got ${lines.length}`);
95
+ }
96
+ const rows = lines.map((lineArg) => `${lineArg.vatId},${zmEurosOf(lineArg.baseCents)},${lineArg.kind}`);
97
+ const text = [...ZM_CSV_HEAD, ...rows].join('\n') + '\n';
98
+ return { text, bytes: new TextEncoder().encode(text), lineCount: rows.length };
99
+ }
100
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiem0uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy96bS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUF5REEsOEhBQThIO0FBQzlILE1BQU0sQ0FBQyxNQUFNLGdCQUFnQixHQUFzQjtJQUNqRCxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUk7Q0FDakssQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUM7QUFFakMsbUZBQW1GO0FBQ25GLE1BQU0sQ0FBQyxNQUFNLFdBQVcsR0FBc0I7SUFDNUMsT0FBTztJQUNQLFVBQVU7SUFDViw4RUFBOEU7Q0FDL0UsQ0FBQztBQUVGOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sQ0FBQyxNQUFNLGFBQWEsR0FDeEIsc01BQXNNLENBQUM7QUFFek07Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxTQUFTLENBQUMsUUFBZ0I7SUFDeEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUNwQyxNQUFNLElBQUksS0FBSyxDQUFDLHNEQUFzRCxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLENBQUM7SUFDRCwyRkFBMkY7SUFDM0YsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLEdBQUcsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQzdDLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsRUFBRSxFQUFFLENBQUM7UUFDeEMsTUFBTSxJQUFJLEtBQUssQ0FBQyxpREFBaUQsS0FBSyxRQUFRLENBQUMsQ0FBQztJQUNsRixDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUM7QUFDZixDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sVUFBVSxnQkFBZ0IsQ0FBQyxRQUFnQjtJQUMvQyxJQUFJLEVBQUUsR0FBRyxNQUFNLENBQUMsUUFBUSxJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDbEUsSUFBSSxFQUFFLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDeEIsRUFBRSxHQUFHLEtBQUssRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQzFCLENBQUM7SUFDRCxJQUFJLEVBQUUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxNQUFNLEdBQUcsRUFBRSxFQUFFLENBQUM7UUFDcEMsTUFBTSxJQUFJLEtBQUssQ0FBQyw0Q0FBNEMsRUFBRSxHQUFHLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBQ0QsSUFBSSxDQUFDLHVCQUF1QixDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDO1FBQ3RDLE1BQU0sSUFBSSxLQUFLLENBQUMseUVBQXlFLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDbEcsQ0FBQztJQUNELElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQy9DLE1BQU0sSUFBSSxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsbUVBQW1FLENBQUMsQ0FBQztJQUN6RyxDQUFDO0lBQ0QsT0FBTyxFQUFFLENBQUM7QUFDWixDQUFDO0FBRUQsa0hBQWtIO0FBQ2xILE1BQU0sVUFBVSxnQkFBZ0IsQ0FBQyxXQUFzQztJQUNyRSxNQUFNLEtBQUssR0FBRyxJQUFJLEdBQUcsRUFBbUIsQ0FBQztJQUN6QyxLQUFLLE1BQU0sTUFBTSxJQUFJLFdBQVcsRUFBRSxDQUFDO1FBQ2pDLElBQUksTUFBTSxDQUFDLElBQUksS0FBSyxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksS0FBSyxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksS0FBSyxHQUFHLEVBQUUsQ0FBQztZQUN0RSxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMxRSxDQUFDO1FBQ0QsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUM7WUFDNUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxxREFBcUQsTUFBTSxDQUFDLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFDM0YsQ0FBQztRQUNELE1BQU0sS0FBSyxHQUFHLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUM3QyxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksTUFBTSxDQUFDLElBQUksS0FBSyxHQUFHLEVBQUUsQ0FBQztZQUNsRCxNQUFNLElBQUksS0FBSyxDQUNiLElBQUksS0FBSyxvUEFBb1AsQ0FDOVAsQ0FBQztRQUNKLENBQUM7UUFDRCxNQUFNLEdBQUcsR0FBRyxHQUFHLEtBQUssSUFBSSxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDdEMsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksRUFBRSxTQUFTLEVBQUUsQ0FBQyxFQUFFLENBQUM7UUFDMUUsSUFBSSxDQUFDLFNBQVMsSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDO1FBQ25DLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxDQUFDO0lBQ3ZCLENBQUM7SUFDRCxPQUFPLENBQUMsR0FBRyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztBQUM3QixDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLFVBQVUsQ0FBQyxRQUE0QixFQUFFLFVBQWtEO0lBQ3pHLE1BQU0sS0FBSyxHQUFHLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLE9BQU8sS0FBSyxZQUFZLElBQUksT0FBTyxDQUFDLFNBQVMsS0FBSyxDQUFDLENBQUMsQ0FBQztJQUM3SCxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdkIsTUFBTSxJQUFJLEtBQUssQ0FBQyxzRkFBc0YsQ0FBQyxDQUFDO0lBQzFHLENBQUM7SUFDRCxJQUFJLEtBQUssQ0FBQyxNQUFNLEdBQUcsWUFBWSxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLEtBQUssQ0FBQyxzQ0FBc0MsWUFBWSxlQUFlLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBQ25HLENBQUM7SUFDRCxNQUFNLElBQUksR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLElBQUksU0FBUyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsSUFBSSxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN4RyxNQUFNLElBQUksR0FBRyxDQUFDLEdBQUcsV0FBVyxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksQ0FBQztJQUN6RCxPQUFPLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLFdBQVcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxTQUFTLEVBQUUsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO0FBQ2pGLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/tax",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "German tax computation engine: UStVA Kennzahl aggregation from ledger balances with form rounding, Zahllast and reconciliation checks — exact cents",
6
6
  "main": "dist_ts/index.js",
@@ -29,12 +29,12 @@
29
29
  "homepage": "https://code.foss.global/fin.cx/tax#readme",
30
30
  "dependencies": {
31
31
  "@fin.cx/calculation": "^1.1.1",
32
- "@fin.cx/chartdata": "^2.2.0"
32
+ "@fin.cx/chartdata": "^2.6.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@git.zone/tsbuild": "^5.0.0",
36
- "@git.zone/tsrun": "^3.0.0",
37
- "@git.zone/tstest": "^6.3.0",
36
+ "@git.zone/tsrun": "^3.0.1",
37
+ "@git.zone/tstest": "^6.3.2",
38
38
  "@types/node": "^26.6.2"
39
39
  },
40
40
  "files": [
package/readme.md CHANGED
@@ -12,7 +12,7 @@ pnpm install @fin.cx/tax
12
12
 
13
13
  `computeUstva()` turns one period's account movements into the UStVA figures:
14
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.
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, 87, 41, 43, 48, 60, 21, 45); Vorsteuer accounts feed the tax fields (Kz 66, 61, 67, 62) cent-exact. `USTVA_KENNZAHL_ORDER` lists the fields in the order of the UStVA 2026 form, with their labels in `USTVA_KENNZAHL_LABELS`; a Kennzahl the chart data maps that has no place there is a filing issue, never dropped.
16
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
17
  - **Output-VAT accounts** (`VAT_ACCOUNTS`, overridable per call with `vatAccounts`) follow the DATEV Kontenrahmen 2026:
18
18
 
@@ -25,7 +25,7 @@ pnpm install @fin.cx/tax
25
25
  | Umsatzsteuer nach § 13b UStG 19 % | 1787 | 3837 |
26
26
  | igE of new vehicles from suppliers without a VAT ID (flagged) | 1784 | 3834 |
27
27
 
28
- - **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)`.
28
+ - **igE and inbound §13b bases** (Kz 89/93, 46/47, 73/74, 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`): `reverseChargeServicesEuCents` for § 13b Abs. 1 (Kz 46), `reverseChargeRealEstateCents` for the Umsätze under the GrEStG, § 13b Abs. 2 Nr. 3 (Kz 73), `reverseChargeOtherCents` for the other § 13b Abs. 2 supplies (Kz 84); the tax fields split the ledger's § 13b output tax, each 19 % of its base and the last the rest, so they sum to the ledger, and the reconciliation checks Kz 67 against Kz 47+74+85. The fields carry the texts of section D of the USt 1 A 2026 (BMF, 29.12.2025), character for character: Kz 46 "Sonstige Leistungen nach § 3a Absatz 2 UStG eines im übrigen Gemeinschaftsgebiet ansässigen Unternehmers (§ 13b Absatz 1 UStG)", Kz 73 "Umsätze, die unter das GrEStG fallen (§ 13b Absatz 2 Nummer 3 UStG)", Kz 84 "Andere Leistungen (§ 13b Absatz 2 Nummer 1, 2, 4 bis 12 UStG)". Without scenario bases the module derives the 19 % bases from the output-tax account movements and marks them `(derived)`.
29
29
  - **Kz 83 (Zahllast/Erstattung)** is computed the way the form computes it, including an optional Sondervorauszahlung credit (Kz 39, Dauerfristverlängerung).
30
30
  - **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.
31
31
  - **Loud failures**: revenue accounts without a UStVA mapping, unknown revenue-class accounts, undifferentiated output-VAT balances, and output VAT on the intra-EU acquisition of new vehicles from suppliers without a VAT ID (SKR 03 1784, SKR 04 3834, which this module does not compute) land in `issues`. **Do not generate a filing while `issues` is non-empty.**
@@ -67,6 +67,18 @@ This module aggregates whatever movements you pass in — it does not decide *wh
67
67
 
68
68
  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.
69
69
 
70
+ ## Zusammenfassende Meldung (§ 18a UStG)
71
+
72
+ `aggregateZmLines(supplies)` sums a period's intra-Community supplies (`L`), § 3a Abs. 2 services (`S`) and triangular trade (`D`) per recipient VAT ID and kind, a later change of the base netted in; a service to a Northern Irish VAT ID (`XI`) is refused, since Northern Ireland is Community territory for goods only (BZSt ZM FAQ, Brexit). `writeZmCsv(lines, { purpose })` writes the CSV that the ZM online form in ELSTER (or the BZSt-Online-Portal) imports: CSV version 3.0, UTF-8, `#v3.0`, `#ve3.2.1`, the field names, then one line per recipient and kind, at most 1 500 lines. An original report leaves out lines that net to zero and refuses to be empty (no "Nullmeldung"); a correction keeps `0` lines, which storno what was reported. `normalizeZmVatId` takes a VAT ID as the form does (`GR` as `EL`, no spaces, a country code of the list). Transmission stays with the person: § 18a requires it authenticated.
73
+
74
+ **Whole euros:** the CSV takes "eine ganze Zahl" of euros, and no source read says how cents are treated. `zmEurosOf` drops the cents of each line's sum toward zero, as the UStVA drops those of its bases (2 000,50 € is 2000, -2 000,50 € is -2000). This is a recorded decision, pending the tax advisor's confirmation (`ZM_EUROS_RULE`), not a rule a source states.
75
+
76
+ ## German VAT rates, dated
77
+
78
+ `GERMAN_VAT_RATES` lists the rates German law sets for a supply, each with the days it applies to, which rate it is (`provision`) and the provision and act that set it (`basis`): 19 % and 7 % (§ 12 Abs. 1, 2 UStG), 16 % and 5 % in their place from 2020-07-01 to 2020-12-31 (§ 28 Abs. 1, 2 UStG), 0 % from 2023-01-01 for the photovoltaic supplies of § 12 Abs. 3 UStG (`provision: 'zero'`, Art. 16 Nr. 5 Jahressteuergesetz 2022), and the average rates of § 24 Abs. 1 Satz 1 UStG: Nr. 1 (5,5 %), Nr. 2 (19 %, 16 % in the second half of 2020 by § 28 Abs. 3 UStG) and Nr. 3. `germanVatRatesOn(day)` answers the rates of a day of supply, each once (§ 27 Abs. 1 Satz 1 UStG: the rate in force when the supply is carried out), `undefined` before 2007-01-01, where the table begins.
79
+
80
+ `GERMAN_VAT_RATE_CONCESSIONS` and `germanVatRateConcessionsOn(day)` hold what the BMF letters of 30 June 2020 and 4 November 2020 say about the rates around the changes of 2020 and 2021, each with its GZ, Rz., the supplies it covers and its standing: `required` where the letter says the rate is to be applied (Beherbergungen in the night of 30 June 2020, Rz. 22 of 4 November 2020), `accepted` where it allows it or does not object (Bewirtung in that night, Rz. 44; 19 % / 7 % on supplies to entrepreneurs in July 2020 with the deduction of the stated tax, Rz. 46; 16 % / 5 % in the night of 31 December 2020, Rz. 22). Whether a supply is one they cover is the caller's fact. No letter on the § 24 changes of 2022 to 2025 was found.
81
+
70
82
  ## Scope
71
83
 
72
84
  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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/tax',
6
- version: '2.0.0',
6
+ version: '2.2.0',
7
7
  description: 'German tax computation engine: UStVA Kennzahl aggregation from ledger balances with form rounding, Zahllast and reconciliation checks — exact cents'
8
8
  }
package/ts/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './ustva.types.js';
2
2
  export * from './ustva.accounts.js';
3
3
  export * from './ustva.js';
4
+ export * from './zm.js';
5
+ export * from './rates.js';
package/ts/rates.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The rates of German VAT, dated: which rates German law sets for a supply
3
+ * carried out on a day, and the transition rules the administration granted
4
+ * around the rate changes of 2020 and 2021. The rate in force when the supply
5
+ * is carried out applies (§ 27 Abs. 1 Satz 1 UStG).
6
+ */
7
+
8
+ /** A rate German law sets for a supply, and the days it applies to: from `from`, until `to` inclusive when it ended. */
9
+ export interface IGermanVatRate {
10
+ ratePercent: number;
11
+ /** `YYYY-MM-DD` */
12
+ from: string;
13
+ /** `YYYY-MM-DD`, inclusive; absent while the rate is in force */
14
+ to?: string;
15
+ /** the rate this is: the general rate, the reduced rate, the 0 % of § 12 Abs. 3 UStG, or an average rate of § 24 Abs. 1 Satz 1 UStG by its number */
16
+ provision: 'general' | 'reduced' | 'zero' | 'average_nr1' | 'average_nr2' | 'average_nr3';
17
+ /** the provision that sets the rate for these days, and the act that set it */
18
+ basis: string;
19
+ }
20
+
21
+ const GENERAL = '§ 12 Abs. 1 UStG (Art. 4 Nr. 1 Haushaltsbegleitgesetz 2006, BGBl. I S. 1402)';
22
+ const REDUCED = '§ 12 Abs. 2 UStG';
23
+ const TEMPORARY = '§ 28 Abs. 1, 2 UStG (Art. 3 Zweites Corona-Steuerhilfegesetz, BGBl. I 2020 S. 1512)';
24
+ const SPECIAL_AVERAGE = '§ 24 Abs. 1 Satz 1 Nr. 2 UStG';
25
+ const FORESTRY = '§ 24 Abs. 1 Satz 1 Nr. 1 UStG (Art. 4 Nr. 2 Buchst. a Doppelbuchst. aa Haushaltsbegleitgesetz 2006)';
26
+ const AVERAGE_OTHER = '§ 24 Abs. 1 Satz 1 Nr. 3 UStG';
27
+ const ZERO = '§ 12 Abs. 3 UStG (Art. 16 Nr. 5 Jahressteuergesetz 2022 vom 16.12.2022, BGBl. I S. 2294, in force 2023-01-01 by its Art. 43)';
28
+
29
+ /**
30
+ * The rates of German VAT, dated. The table begins on 2007-01-01.
31
+ * - The general rate, 19 % (§ 12 Abs. 1 UStG, since Art. 4 Nr. 1
32
+ * Haushaltsbegleitgesetz 2006, BGBl. I S. 1402, from 2007-01-01), and the
33
+ * reduced rate, 7 % (§ 12 Abs. 2 UStG). From 2020-07-01 to 2020-12-31, 16 %
34
+ * and 5 % in their place (§ 28 Abs. 1, 2 UStG, Art. 3 Zweites
35
+ * Corona-Steuerhilfegesetz, BGBl. I 2020 S. 1512).
36
+ * - § 24 Abs. 1 Satz 1 Nr. 2 UStG (Sägewerkserzeugnisse, Getränke and
37
+ * alkoholische Flüssigkeiten of a farmer or forester taxed by average
38
+ * rates): 19 %, the rate it states as read on gesetze-im-internet.de on
39
+ * 2026-09-27; 16 % from 2020-07-01 to 2020-12-31 (§ 24 Abs. 1 Satz 1 Nr. 2
40
+ * i. V. m. § 28 Abs. 3 UStG, Art. 3 Zweites Corona-Steuerhilfegesetz, BGBl. I
41
+ * 2020 S. 1512, as the BMF letter of 30 June 2020, Rz. 1, cites it; the act's
42
+ * own text was not read). That 19 % applied from 2007-01-01 is not verified
43
+ * from the amending act: the table starts it there with the general rate.
44
+ * - The average rates of § 24 Abs. 1 Satz 1 UStG, at which a farmer or
45
+ * forester taxed by them charges VAT on the invoice; the recipient deducts
46
+ * it as input tax (§ 15 Abs. 1 Satz 1 Nr. 1 UStG). Each from the amending
47
+ * act's text:
48
+ * - Nr. 1, forestry products: 5,5 % from 2007-01-01 (Art. 4 Nr. 2
49
+ * Buchst. a Doppelbuchst. aa Haushaltsbegleitgesetz 2006, "5 Prozent"
50
+ * replaced by "5,5 Prozent"); unchanged since.
51
+ * - Nr. 3, the other supplies: 10,7 % from 2007-01-01 (the same, Doppelbuchst.
52
+ * cc, "9 Prozent" by "10,7 Prozent"); 9,5 % from 2022-01-01 (Art. 1 Nr. 5
53
+ * Gesetz zur Umsetzung unionsrechtlicher Vorgaben im Umsatzsteuerrecht of
54
+ * 21.12.2021, BGBl. I S. 5250); 9,0 % from 2023-01-01 (Art. 12 Nr. 3 Achtes
55
+ * Gesetz zur Änderung von Verbrauchsteuergesetzen of 24.10.2022, BGBl. I
56
+ * S. 1838); 8,4 % from 2024-12-06 (Art. 24 Nr. 13 Jahressteuergesetz 2024
57
+ * of 02.12.2024, BGBl. 2024 I Nr. 387, in force the day after its
58
+ * promulgation on 2024-12-05, Art. 56 Abs. 1); 7,8 % from 2025-01-01
59
+ * (Art. 25 Nr. 21 of the same, Art. 56 Abs. 7), the rate § 24 Abs. 1 Satz 1
60
+ * Nr. 3 UStG states as read on gesetze-im-internet.de on 2026-09-25.
61
+ * - 0 % for the supplies of § 12 Abs. 3 UStG (photovoltaic installations:
62
+ * the modules and components delivered to their operator, their
63
+ * intra-Community acquisition, import and installation) from 2023-01-01
64
+ * (Art. 16 Nr. 5 Jahressteuergesetz 2022 of 16.12.2022, BGBl. I S. 2294,
65
+ * in force on 2023-01-01 by its Art. 43; the amending text as read on
66
+ * buzer.de on 2026-09-28, § 12 Abs. 3 as read on gesetze-im-internet.de).
67
+ */
68
+ export const GERMAN_VAT_RATES: readonly IGermanVatRate[] = [
69
+ { ratePercent: 19, from: '2007-01-01', to: '2020-06-30', provision: 'general', basis: GENERAL },
70
+ { ratePercent: 19, from: '2021-01-01', provision: 'general', basis: GENERAL },
71
+ { ratePercent: 7, from: '2007-01-01', to: '2020-06-30', provision: 'reduced', basis: REDUCED },
72
+ { ratePercent: 7, from: '2021-01-01', provision: 'reduced', basis: REDUCED },
73
+ { ratePercent: 0, from: '2023-01-01', provision: 'zero', basis: ZERO },
74
+ { ratePercent: 16, from: '2020-07-01', to: '2020-12-31', provision: 'general', basis: `${TEMPORARY}, in place of the rate of § 12 Abs. 1 UStG` },
75
+ { ratePercent: 5, from: '2020-07-01', to: '2020-12-31', provision: 'reduced', basis: `${TEMPORARY}, in place of the rate of § 12 Abs. 2 UStG` },
76
+ { ratePercent: 19, from: '2007-01-01', to: '2020-06-30', provision: 'average_nr2', basis: `${SPECIAL_AVERAGE} (the act that set 19 % from 2007-01-01 not verified)` },
77
+ { ratePercent: 19, from: '2021-01-01', provision: 'average_nr2', basis: `${SPECIAL_AVERAGE} (19 % as read on gesetze-im-internet.de, 2026-09-27)` },
78
+ { ratePercent: 16, from: '2020-07-01', to: '2020-12-31', provision: 'average_nr2', basis: `${SPECIAL_AVERAGE} i. V. m. § 28 Abs. 3 UStG (Art. 3 Zweites Corona-Steuerhilfegesetz, BGBl. I 2020 S. 1512, as cited by BMF 30.06.2020 Rz. 1)` },
79
+ { ratePercent: 5.5, from: '2007-01-01', provision: 'average_nr1', basis: FORESTRY },
80
+ { ratePercent: 10.7, from: '2007-01-01', to: '2021-12-31', provision: 'average_nr3', basis: `${AVERAGE_OTHER} (Art. 4 Nr. 2 Buchst. a Doppelbuchst. cc Haushaltsbegleitgesetz 2006)` },
81
+ { ratePercent: 9.5, from: '2022-01-01', to: '2022-12-31', provision: 'average_nr3', basis: `${AVERAGE_OTHER} (Art. 1 Nr. 5 Gesetz zur Umsetzung unionsrechtlicher Vorgaben im Umsatzsteuerrecht, BGBl. I 2021 S. 5250)` },
82
+ { ratePercent: 9, from: '2023-01-01', to: '2024-12-05', provision: 'average_nr3', basis: `${AVERAGE_OTHER} (Art. 12 Nr. 3 Achtes Gesetz zur Änderung von Verbrauchsteuergesetzen, BGBl. I 2022 S. 1838)` },
83
+ { ratePercent: 8.4, from: '2024-12-06', to: '2024-12-31', provision: 'average_nr3', basis: `${AVERAGE_OTHER} (Art. 24 Nr. 13 Jahressteuergesetz 2024, BGBl. 2024 I Nr. 387)` },
84
+ { ratePercent: 7.8, from: '2025-01-01', provision: 'average_nr3', basis: `${AVERAGE_OTHER} (Art. 25 Nr. 21 Jahressteuergesetz 2024, BGBl. 2024 I Nr. 387)` },
85
+ ];
86
+
87
+ const TABLE_BEGINS = '2007-01-01';
88
+
89
+ const assertDay = (dayArg: string): void => {
90
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dayArg);
91
+ const date = match ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : undefined;
92
+ if (!date || date.toISOString().slice(0, 10) !== dayArg) {
93
+ throw new Error(`a day of supply is written YYYY-MM-DD, got '${dayArg}'`);
94
+ }
95
+ };
96
+
97
+ /**
98
+ * The rates of German VAT for a supply carried out on a day (`YYYY-MM-DD`),
99
+ * each once, in the order of `GERMAN_VAT_RATES`. `undefined` for a day before
100
+ * the table begins (2007-01-01): the table holds no law for it.
101
+ */
102
+ export function germanVatRatesOn(dayArg: string): readonly number[] | undefined {
103
+ assertDay(dayArg);
104
+ if (dayArg < TABLE_BEGINS) {
105
+ return undefined;
106
+ }
107
+ return [...new Set(GERMAN_VAT_RATES
108
+ .filter((rateArg) => rateArg.from <= dayArg && (rateArg.to === undefined || dayArg <= rateArg.to))
109
+ .map((rateArg) => rateArg.ratePercent))];
110
+ }
111
+
112
+ /**
113
+ * A rate the administration names for some supplies of some days around a
114
+ * change of the rates, with its source and the supplies it covers. Whether a
115
+ * supply is one of them is a fact the caller states; this table cannot tell.
116
+ */
117
+ export interface IGermanVatRateConcession {
118
+ ratePercent: number;
119
+ /**
120
+ * `required`: the letter says the rate is to be applied ("sind … anzuwenden");
121
+ * `accepted`: the letter allows it or does not object to it ("wird
122
+ * zugelassen", "wird nicht beanstandet"), and the law's rate of the day may be
123
+ * applied as well.
124
+ */
125
+ standing: 'required' | 'accepted';
126
+ /** `YYYY-MM-DD` */
127
+ from: string;
128
+ /** `YYYY-MM-DD`, inclusive */
129
+ to: string;
130
+ /** the BMF letter, its GZ and its Rz. */
131
+ source: string;
132
+ /** the supplies it covers, and what it allows */
133
+ covers: string;
134
+ }
135
+
136
+ const BMF_2020_06_30 = 'BMF-Schreiben vom 30. Juni 2020, III C 2 - S 7030/20/10009 :004, BStBl I S. 584';
137
+ const BMF_2020_11_04 = 'BMF-Schreiben vom 4. November 2020, III C 2 - S 7030/20/10009 :016';
138
+ const NIGHT_2020_06_30_BEWIRTUNG = 'Bewirtungsleistungen (food and drink for consumption on the spot, tobacco and the like) in the night of 30 June to 1 July 2020, at the rates in force from 1 July 2020 ("wird zugelassen")';
139
+ const NIGHT_2020_06_30_BEHERBERGUNG = 'Beherbergungen and the services that go with them in the night of 30 June to 1 July 2020, at the rates in force from 1 July 2020 ("sind … anzuwenden"; the letter of 30 June 2020, Rz. 44, had left them out)';
140
+ const JULY_2020 = '("wird es … nicht beanstandet") a supply to an entrepreneur carried out in July 2020 whose invoice states the rate in force before 1 July 2020 and whose supplier paid that tax: the invoice need not be corrected, and a recipient entitled to deduct input tax deducts the tax as stated (for § 13b supplies, the tax the recipient computed)';
141
+ const NIGHT_2020_12_31 = 'Bewirtungsleistungen, and Beherbergungen with the services that go with them, in the night of 31 December 2020 to 1 January 2021, at the rates of 16 and 5 percent ("wird zugelassen")';
142
+
143
+ /**
144
+ * The transition rules of the administration around the rate changes of
145
+ * 1 July 2020 and 1 January 2021, each read in the letter itself (the copies of
146
+ * 8 July 2020 and 5 November 2020 in the Internet Archive of the PDFs on
147
+ * bundesfinanzministerium.de, read 2026-09-27):
148
+ * - Rz. 44 of the letter of 30 June 2020 allows the new rates on
149
+ * Bewirtungsleistungen in the night of 30 June to 1 July 2020 (accepted), and
150
+ * Rz. 22 of the letter of 4 November 2020 requires them on Beherbergungen of
151
+ * that night (required), which Rz. 44 had left out.
152
+ * - Rz. 46 of the letter of 30 June 2020 accepts the old rates, 19 and
153
+ * 7 percent, on supplies to entrepreneurs in July 2020, with the input tax
154
+ * deduction of the stated tax.
155
+ * - Rz. 22 of the letter of 4 November 2020 allows 16 and 5 percent on
156
+ * Bewirtungsleistungen and Beherbergungen in the night of 31 December 2020
157
+ * to 1 January 2021.
158
+ * Not here: Rz. 47 of the letter of 30 June 2020 applies its rules "entsprechend"
159
+ * to the rise on 1 January 2021; the letters grant no deduction of 16 or
160
+ * 5 percent stated on a supply of January 2021 beyond the night above, except
161
+ * for advance payment invoices issued before 2021 (4 November 2020, Rz. 1 and
162
+ * 2), whose case needs the invoice's date. No letter on the § 24 changes of
163
+ * 2022, 2023, 2024-12-06 and 2025 was found.
164
+ */
165
+ export const GERMAN_VAT_RATE_CONCESSIONS: readonly IGermanVatRateConcession[] = [
166
+ { ratePercent: 16, standing: 'accepted', from: '2020-06-30', to: '2020-06-30', source: `${BMF_2020_06_30}, Rz. 44; ${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_06_30_BEWIRTUNG },
167
+ { ratePercent: 5, standing: 'accepted', from: '2020-06-30', to: '2020-06-30', source: `${BMF_2020_06_30}, Rz. 44; ${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_06_30_BEWIRTUNG },
168
+ { ratePercent: 16, standing: 'required', from: '2020-06-30', to: '2020-06-30', source: `${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_06_30_BEHERBERGUNG },
169
+ { ratePercent: 5, standing: 'required', from: '2020-06-30', to: '2020-06-30', source: `${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_06_30_BEHERBERGUNG },
170
+ { ratePercent: 19, standing: 'accepted', from: '2020-07-01', to: '2020-07-31', source: `${BMF_2020_06_30}, Rz. 46`, covers: JULY_2020 },
171
+ { ratePercent: 7, standing: 'accepted', from: '2020-07-01', to: '2020-07-31', source: `${BMF_2020_06_30}, Rz. 46`, covers: JULY_2020 },
172
+ { ratePercent: 16, standing: 'accepted', from: '2021-01-01', to: '2021-01-01', source: `${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_12_31 },
173
+ { ratePercent: 5, standing: 'accepted', from: '2021-01-01', to: '2021-01-01', source: `${BMF_2020_11_04}, Rz. 22`, covers: NIGHT_2020_12_31 },
174
+ ];
175
+
176
+ /** The transition rules that cover a day of supply (`YYYY-MM-DD`), in the order of `GERMAN_VAT_RATE_CONCESSIONS`. */
177
+ export function germanVatRateConcessionsOn(dayArg: string): readonly IGermanVatRateConcession[] {
178
+ assertDay(dayArg);
179
+ return GERMAN_VAT_RATE_CONCESSIONS.filter((entryArg) => entryArg.from <= dayArg && dayArg <= entryArg.to);
180
+ }
package/ts/ustva.ts CHANGED
@@ -29,19 +29,30 @@ import type {
29
29
  TKennzahlKind,
30
30
  } from './ustva.types.js';
31
31
 
32
- const KENNZAHL_LABELS: Record<number, string> = {
32
+ /**
33
+ * The labels of the Kennzahlen this module fills. Kz 21, 45 and 87 carry the
34
+ * titles of their fields in ELSTER's "Anleitung zur UStVA 2026" (read
35
+ * 2026-09-25); Kz 46, 73 and 84, the fields of section D of the USt 1 A 2026
36
+ * (BMF, 29.12.2025), their texts there.
37
+ */
38
+ export const USTVA_KENNZAHL_LABELS: Readonly<Record<number, string>> = {
33
39
  81: 'Steuerpflichtige Umsätze 19 %',
34
40
  86: 'Steuerpflichtige Umsätze 7 %',
41
+ 87: 'Steuerpflichtige Lieferungen und sonstige Leistungen (0 %)',
35
42
  89: 'Innergemeinschaftliche Erwerbe 19 %',
36
43
  93: 'Innergemeinschaftliche Erwerbe 7 %',
37
44
  41: 'Steuerfreie innergemeinschaftliche Lieferungen',
38
45
  43: 'Steuerfreie Umsätze mit Vorsteuerabzug (u. a. Ausfuhr)',
39
46
  48: 'Steuerfreie Umsätze ohne Vorsteuerabzug',
40
- 46: 'Sonstige Leistungen aus dem übrigen Gemeinschaftsgebiet (§13b)',
47
+ 46: 'Sonstige Leistungen nach § 3a Absatz 2 UStG eines im übrigen Gemeinschaftsgebiet ansässigen Unternehmers (§ 13b Absatz 1 UStG)',
41
48
  47: 'Steuer zu Kennzahl 46',
42
- 84: 'Andere Leistungen eines im Ausland ansässigen Unternehmers (§13b)',
49
+ 73: 'Umsätze, die unter das GrEStG fallen (§ 13b Absatz 2 Nummer 3 UStG)',
50
+ 74: 'Steuer zu Kennzahl 73',
51
+ 84: 'Andere Leistungen (§ 13b Absatz 2 Nummer 1, 2, 4 bis 12 UStG)',
43
52
  85: 'Steuer zu Kennzahl 84',
44
53
  60: 'Steuerpflichtige Umsätze, für die der Leistungsempfänger die Steuer schuldet (§13b)',
54
+ 21: 'Sonstige Leistungen im übrigen Gemeinschaftsgebiet',
55
+ 45: 'Übrige nicht steuerbare Umsätze (Leistungsort nicht im Inland)',
45
56
  66: 'Vorsteuer aus Rechnungen anderer Unternehmer',
46
57
  61: 'Vorsteuer aus innergemeinschaftlichem Erwerb',
47
58
  62: 'Entrichtete Einfuhrumsatzsteuer',
@@ -50,8 +61,17 @@ const KENNZAHL_LABELS: Record<number, string> = {
50
61
  83: 'Verbleibende Umsatzsteuer-Vorauszahlung / Überschuss',
51
62
  };
52
63
 
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];
64
+ /**
65
+ * The Kennzahlen this module fills, in the order of the UStVA 2026 form
66
+ * (ELSTER, "Anleitung zur UStVA 2026"): taxable supplies (81, 86, 87), tax-free
67
+ * supplies (41, 43, 48), intra-Community acquisitions (89, 93), the tax the
68
+ * recipient owes (46/47, 73/74, 84/85), supplies whose recipient owes the tax (60),
69
+ * services in the rest of the Community (21), other non-taxable turnover (45),
70
+ * input tax (66, 61, 62, 67), the Sondervorauszahlung (39) and the result (83).
71
+ * A Kennzahl computed but not listed here is reported as an issue, never
72
+ * dropped.
73
+ */
74
+ export const USTVA_KENNZAHL_ORDER: readonly number[] = [81, 86, 87, 41, 43, 48, 89, 93, 46, 47, 73, 74, 84, 85, 60, 21, 45, 66, 61, 62, 67, 39, 83];
55
75
 
56
76
  /** full euros, cents truncated toward zero (form rule for bases) */
57
77
  function truncateToEur(cents: TCents): number {
@@ -189,9 +209,11 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
189
209
 
190
210
  let kz46Cents = scenario.reverseChargeServicesEuCents ?? 0;
191
211
  let kz46Source = '(scenario)';
212
+ const kz73Cents = scenario.reverseChargeRealEstateCents ?? 0;
192
213
  const kz84Cents = scenario.reverseChargeOtherCents ?? 0;
193
214
  const hasRcScenario =
194
215
  scenario.reverseChargeServicesEuCents !== undefined ||
216
+ scenario.reverseChargeRealEstateCents !== undefined ||
195
217
  scenario.reverseChargeOtherCents !== undefined;
196
218
  if (!hasRcScenario && ledgerRcOutputCents !== 0) {
197
219
  kz46Cents = Math.round((ledgerRcOutputCents * 100) / 19);
@@ -201,19 +223,23 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
201
223
  );
202
224
  }
203
225
 
204
- // §13b tax split: keep the total identical to the ledger movement
205
- let kz47Cents = 0;
206
- let kz85Cents = 0;
207
- if (kz46Cents !== 0 && kz84Cents !== 0) {
208
- kz47Cents = vatFromNetCents(kz46Cents, 19);
209
- kz85Cents = ledgerRcOutputCents - kz47Cents;
210
- } else if (kz84Cents !== 0) {
211
- kz85Cents = ledgerRcOutputCents;
212
- } else if (kz46Cents !== 0) {
213
- kz47Cents = ledgerRcOutputCents;
226
+ // §13b tax split over Kz 47, 74 and 85: each base's 19 %, the last base carrying what keeps the total identical to
227
+ // the ledger movement; a single base carries the whole movement
228
+ const rcTaxCents = new Map<number, number>();
229
+ const rcBases = ([[47, kz46Cents], [74, kz73Cents], [85, kz84Cents]] as Array<[number, number]>).filter(([, cents]) => cents !== 0);
230
+ if (rcBases.length > 0) {
231
+ let rest = ledgerRcOutputCents;
232
+ rcBases.forEach(([taxKennzahl, baseCents], index) => {
233
+ const cents = index === rcBases.length - 1 ? rest : vatFromNetCents(baseCents, 19);
234
+ rcTaxCents.set(taxKennzahl, cents);
235
+ rest -= cents;
236
+ });
214
237
  } else if (ledgerRcOutputCents !== 0) {
215
- kz47Cents = ledgerRcOutputCents; // derived path above set Kz 46
238
+ rcTaxCents.set(47, ledgerRcOutputCents); // derived path above set Kz 46
216
239
  }
240
+ const kz47Cents = rcTaxCents.get(47) ?? 0;
241
+ const kz74Cents = rcTaxCents.get(74) ?? 0;
242
+ const kz85Cents = rcTaxCents.get(85) ?? 0;
217
243
 
218
244
  // consistency: scenario bases × rate must reproduce the ledger tax
219
245
  if (kz89Source === '(scenario)' && kz89Cents !== 0) {
@@ -225,7 +251,7 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
225
251
  }
226
252
  }
227
253
  if (hasRcScenario) {
228
- const expected = vatFromNetCents(kz46Cents, 19) + vatFromNetCents(kz84Cents, 19);
254
+ const expected = vatFromNetCents(kz46Cents, 19) + vatFromNetCents(kz73Cents, 19) + vatFromNetCents(kz84Cents, 19);
229
255
  const diff = ledgerRcOutputCents - expected;
230
256
  checks.push(`§13b: ledger output tax ${ledgerRcOutputCents} vs 19 % of scenario bases ${expected} (diff ${diff})`);
231
257
  if (Math.abs(diff) > tolerance) {
@@ -242,7 +268,7 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
242
268
  kennzahl,
243
269
  kind,
244
270
  value,
245
- label: KENNZAHL_LABELS[kennzahl] ?? `Kennzahl ${kennzahl}`,
271
+ label: USTVA_KENNZAHL_LABELS[kennzahl] ?? `Kennzahl ${kennzahl}`,
246
272
  accounts,
247
273
  });
248
274
  };
@@ -257,6 +283,8 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
257
283
  if (kz93Cents !== 0) put(93, 'baseEur', truncateToEur(kz93Cents), ['(scenario)']);
258
284
  if (kz46Cents !== 0) put(46, 'baseEur', truncateToEur(kz46Cents), [kz46Source]);
259
285
  if (kz47Cents !== 0) put(47, 'taxCents', kz47Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
286
+ if (kz73Cents !== 0) put(73, 'baseEur', truncateToEur(kz73Cents), ['(scenario)']);
287
+ if (kz74Cents !== 0) put(74, 'taxCents', kz74Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
260
288
  if (kz84Cents !== 0) put(84, 'baseEur', truncateToEur(kz84Cents), ['(scenario)']);
261
289
  if (kz85Cents !== 0) put(85, 'taxCents', kz85Cents, VAT_ACCOUNTS[input.chart].outputReverseCharge);
262
290
  const sondervorauszahlung = input.sondervorauszahlungCents ?? 0;
@@ -273,6 +301,7 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
273
301
  formDomesticTaxCents +
274
302
  formIgeTaxCents +
275
303
  kz47Cents +
304
+ kz74Cents +
276
305
  kz85Cents -
277
306
  inputTaxCents -
278
307
  sondervorauszahlung;
@@ -296,16 +325,26 @@ export function computeUstva(input: IUstvaInput): IUstvaResult {
296
325
  `igE input vs output tax: Kz 61 ${kzValue(61)} vs ledger igE output ${ledgerIgeOutputCents} (diff ${igeDeductionDiff}; non-zero = partial deduction or inconsistency)`,
297
326
  );
298
327
  }
299
- const rcDeductionDiff = kzValue(67) - (kz47Cents + kz85Cents);
300
- if (kzValue(67) !== 0 || kz47Cents !== 0 || kz85Cents !== 0) {
328
+ // every § 13b tax field the recipient owes, the GrEStG's Kz 74 included, against its deduction
329
+ const rcOutputCents = kz47Cents + kz74Cents + kz85Cents;
330
+ const rcDeductionDiff = kzValue(67) - rcOutputCents;
331
+ if (kzValue(67) !== 0 || kz47Cents !== 0 || kz74Cents !== 0 || kz85Cents !== 0) {
301
332
  checks.push(
302
- `§13b input vs output tax: Kz 67 ${kzValue(67)} vs Kz 47+85 ${kz47Cents + kz85Cents} (diff ${rcDeductionDiff}; non-zero = partial deduction or inconsistency)`,
333
+ `§13b input vs output tax: Kz 67 ${kzValue(67)} vs Kz 47+74+85 ${rcOutputCents} (diff ${rcDeductionDiff}; non-zero = partial deduction or inconsistency)`,
303
334
  );
304
335
  }
305
336
 
306
- const kennzahlen = KENNZAHL_ORDER.filter((kennzahl) => values.has(kennzahl)).map(
337
+ const kennzahlen = USTVA_KENNZAHL_ORDER.filter((kennzahl) => values.has(kennzahl)).map(
307
338
  (kennzahl) => values.get(kennzahl)!,
308
339
  );
340
+ // a figure the form order does not place would be left out of the filing: say so instead
341
+ for (const [kennzahl, value] of values) {
342
+ if (!USTVA_KENNZAHL_ORDER.includes(kennzahl)) {
343
+ issues.push(
344
+ `Kennzahl ${kennzahl} (from ${value.accounts.join(', ')}) has no place in this module's form order — it cannot be filed; add it to USTVA_KENNZAHL_ORDER`,
345
+ );
346
+ }
347
+ }
309
348
 
310
349
  return {
311
350
  chart: input.chart,