@fin.cx/tax 2.2.0 → 2.3.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/tax',
6
- version: '2.2.0',
6
+ version: '2.3.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
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxhQUFhO0lBQ25CLE9BQU8sRUFBRSxPQUFPO0lBQ2hCLFdBQVcsRUFBRSxxSkFBcUo7Q0FDbkssQ0FBQSJ9
@@ -3,3 +3,4 @@ export * from './ustva.accounts.js';
3
3
  export * from './ustva.js';
4
4
  export * from './zm.js';
5
5
  export * from './rates.js';
6
+ export * from './smallbusiness.js';
package/dist_ts/index.js CHANGED
@@ -3,4 +3,5 @@ export * from './ustva.accounts.js';
3
3
  export * from './ustva.js';
4
4
  export * from './zm.js';
5
5
  export * from './rates.js';
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxTQUFTLENBQUM7QUFDeEIsY0FBYyxZQUFZLENBQUMifQ==
6
+ export * from './smallbusiness.js';
7
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxTQUFTLENBQUM7QUFDeEIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxvQkFBb0IsQ0FBQyJ9
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Kleinunternehmer of § 19 UStG in the version from 1 January 2025 and
3
+ * its Gesamtumsatz. Pure; the caller states each Entgelt received and what
4
+ * kind of supply it paid, and records the waiver.
5
+ *
6
+ * Source: § 19 UStG on gesetze-im-internet.de, read 2026-09-29.
7
+ * - Abs. 1 Satz 1: a supply (§ 1 Abs. 1 Nr. 1) of a business resident in
8
+ * Germany or in the areas of § 1 Abs. 3 UStG "ist steuerfrei, wenn der Gesamtumsatz nach Absatz 2 im
9
+ * vorangegangenen Kalenderjahr 25 000 Euro nicht überschritten hat und im
10
+ * laufenden Kalenderjahr 100 000 Euro nicht überschreitet".
11
+ * - Abs. 2: "Gesamtumsatz ist die nach vereinnahmten Entgelten berechnete
12
+ * Summe der vom Unternehmer ausgeführten steuerbaren Umsätze im Sinne des
13
+ * § 1 Absatz 1 Nummer 1 abzüglich folgender Umsätze: 1. Umsätze, die nach
14
+ * § 4 Nummer 8 Buchstabe i, Nummer 9 Buchstabe b und Nummer 11 bis 29
15
+ * steuerfrei sind; 2. Umsätze, die nach § 4 Nummer 8 Buchstabe a bis h,
16
+ * Nummer 9 Buchstabe a und Nummer 10 steuerfrei sind, wenn sie Hilfsumsätze
17
+ * sind. Die Umsätze mit Wirtschaftsgütern des Anlagevermögens eines
18
+ * Unternehmers bleiben bei der Ermittlung des Gesamtumsatzes nach Satz 1
19
+ * außer Ansatz."
20
+ * - Abs. 3: the waiver, declared "bis zum letzten Tag des Monats Februar des
21
+ * zweiten auf den Besteuerungszeitraum folgenden Kalenderjahres",
22
+ * effective from the start of the Besteuerungszeitraum it is for, binding
23
+ * "mindestens für fünf Kalenderjahre", revocable "mit Wirkung von Beginn
24
+ * eines darauffolgenden Kalenderjahres".
25
+ *
26
+ * - Abs. 6: not for the intra-Community supply of new vehicles; not modelled
27
+ * here (the caller counts such a supply taxed).
28
+ * - The version until 31 December 2024 had other limits (22 000 € in the
29
+ * previous year, 50 000 € expected in the running one, both with the
30
+ * tax) and did not exempt the supplies but levied no tax on them; a year
31
+ * before 2025 answers `before_2025` and is not told.
32
+ *
33
+ * Not read: the BMF-Schreiben of 18 March 2025 (UStAE Abschn. 19.1 ff.),
34
+ * which the BMF site did not serve. Two questions it answers stay open here:
35
+ * the limit in the year the business starts, which the statute does not name
36
+ * (`business_started_this_year` answers `unknown`), and whether the supply by
37
+ * which the running year exceeds 100 000 € is itself taxed. The statute's
38
+ * wording (the running year's Gesamtumsatz "nicht überschreitet") is read so
39
+ * that it is: `endsWith` names the receipt at which the sum first exceeds the
40
+ * limit, and every receipt from it on is taxed.
41
+ */
42
+ import type { TCents } from '@fin.cx/calculation';
43
+ /** The first calendar year of the version of § 19 UStG this module applies (Jahressteuergesetz 2024, from 1 January 2025). */
44
+ export declare const SMALL_BUSINESS_FIRST_YEAR = 2025;
45
+ /** 25 000 €: the previous calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
46
+ export declare const SMALL_BUSINESS_PREVIOUS_YEAR_LIMIT_CENTS: TCents;
47
+ /** 100 000 €: the running calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
48
+ export declare const SMALL_BUSINESS_RUNNING_YEAR_LIMIT_CENTS: TCents;
49
+ /**
50
+ * What a receipt is to the Gesamtumsatz (§ 19 Abs. 2 UStG):
51
+ * - `counted`: an Entgelt for a steuerbarer Umsatz (§ 1 Abs. 1 Nr. 1), taxed or
52
+ * exempt otherwise than below, a Kleinunternehmer's own exempt supplies
53
+ * included;
54
+ * - `exempt_section_4_listed`: exempt under § 4 Nr. 8 Buchst. i, Nr. 9
55
+ * Buchst. b or Nr. 11 bis 29 (Satz 1 Nr. 1), left out;
56
+ * - `exempt_ancillary`: exempt under § 4 Nr. 8 Buchst. a bis h, Nr. 9 Buchst.
57
+ * a or Nr. 10 and a Hilfsumsatz (Satz 1 Nr. 2), left out; such a supply that
58
+ * is no Hilfsumsatz is `counted`;
59
+ * - `fixed_assets`: a sale of fixed assets (Satz 2), left out;
60
+ * - `not_taxable_here`: no steuerbarer Umsatz in Germany (its place of supply
61
+ * abroad), left out.
62
+ */
63
+ export type TGesamtumsatzPart = 'counted' | 'exempt_section_4_listed' | 'exempt_ancillary' | 'fixed_assets' | 'not_taxable_here';
64
+ export declare const GESAMTUMSATZ_PARTS: readonly TGesamtumsatzPart[];
65
+ /** One Entgelt received (§ 19 Abs. 2 UStG counts by receipts), or a refund of one (negative). */
66
+ export interface IGesamtumsatzReceipt {
67
+ /** the day it was received, `YYYY-MM-DD` */
68
+ receivedOn: string;
69
+ /** the Entgelt, whole cents, without the tax where one is owed on it; negative for a refund */
70
+ entgeltCents: TCents;
71
+ part: TGesamtumsatzPart;
72
+ }
73
+ /** The Gesamtumsatz of a calendar year, and what § 19 Abs. 2 left out of it. */
74
+ export interface IGesamtumsatzOfYear {
75
+ year: number;
76
+ totalCents: TCents;
77
+ leftOutCents: Record<Exclude<TGesamtumsatzPart, 'counted'>, TCents>;
78
+ }
79
+ /** Why a year starts with the exemption or without it. */
80
+ export type TSmallBusinessReason = 'within_limits' | 'previous_year_over' | 'waived' | 'previous_year_unknown' | 'business_started_this_year'
81
+ /** a year before 2025, under the version with other limits (`SMALL_BUSINESS_FIRST_YEAR`): not told */
82
+ | 'before_2025';
83
+ export interface ISmallBusinessYearInput {
84
+ /** the calendar year (the Besteuerungszeitraum, § 16 Abs. 1 Satz 2 UStG) */
85
+ year: number;
86
+ /**
87
+ * the Gesamtumsatz of the previous calendar year, whole cents; `undefined`
88
+ * where it is not known; `'business_started'` where the business started in
89
+ * this year and had no previous year
90
+ */
91
+ previousYearCents: TCents | undefined | 'business_started';
92
+ /** a waiver (§ 19 Abs. 3 UStG) is in force for the year */
93
+ waived?: boolean;
94
+ /** the receipts of the year, in any order; those of other years are ignored */
95
+ receipts: readonly IGesamtumsatzReceipt[];
96
+ }
97
+ export interface ISmallBusinessStatus {
98
+ year: number;
99
+ /** whether the year's supplies are exempt from its start; `unknown` where the input cannot tell */
100
+ atStart: 'exempt' | 'taxable' | 'unknown';
101
+ reason: TSmallBusinessReason;
102
+ /**
103
+ * Where the year starts exempt and its Gesamtumsatz exceeds 100 000 €: the
104
+ * receipt by which it first does (its day, its place in the receipts as
105
+ * given, the sum with it). It and every later receipt are taxed.
106
+ */
107
+ endsWith?: {
108
+ receivedOn: string;
109
+ index: number;
110
+ runningCents: TCents;
111
+ };
112
+ /** the year's Gesamtumsatz over all its receipts */
113
+ runningCents: TCents;
114
+ }
115
+ /** The Gesamtumsatz of a calendar year (§ 19 Abs. 2 UStG): the Entgelte received in it that count, and those left out. */
116
+ export declare function gesamtumsatzOf(receiptsArg: readonly IGesamtumsatzReceipt[], yearArg: number): IGesamtumsatzOfYear;
117
+ /**
118
+ * Whether a calendar year's supplies are exempt under § 19 Abs. 1 UStG: from
119
+ * its start where the previous year's Gesamtumsatz did not exceed 25 000 € and
120
+ * no waiver is in force, until the receipt by which the running year's
121
+ * Gesamtumsatz exceeds 100 000 € (`endsWith`).
122
+ */
123
+ export declare function smallBusinessStatusOf(inputArg: ISmallBusinessYearInput): ISmallBusinessStatus;
124
+ /** The last calendar year a waiver effective for `yearArg` binds (§ 19 Abs. 3 Satz 3 UStG: at least five calendar years). */
125
+ export declare function smallBusinessWaiverBindsUntil(yearArg: number): number;
126
+ /** The first calendar year from whose start a waiver effective for `yearArg` can be revoked (§ 19 Abs. 3 Satz 3 and 4 UStG). */
127
+ export declare function smallBusinessWaiverRevocableFrom(yearArg: number): number;
128
+ /** The last day a waiver for the Besteuerungszeitraum `yearArg` can be declared: the last day of February two years later (§ 19 Abs. 3 Satz 1 UStG). */
129
+ export declare function smallBusinessWaiverDeadline(yearArg: number): string;
@@ -0,0 +1,113 @@
1
+ /** The first calendar year of the version of § 19 UStG this module applies (Jahressteuergesetz 2024, from 1 January 2025). */
2
+ export const SMALL_BUSINESS_FIRST_YEAR = 2025;
3
+ /** 25 000 €: the previous calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
4
+ export const SMALL_BUSINESS_PREVIOUS_YEAR_LIMIT_CENTS = 2_500_000;
5
+ /** 100 000 €: the running calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
6
+ export const SMALL_BUSINESS_RUNNING_YEAR_LIMIT_CENTS = 10_000_000;
7
+ export const GESAMTUMSATZ_PARTS = ['counted', 'exempt_section_4_listed', 'exempt_ancillary', 'fixed_assets', 'not_taxable_here'];
8
+ const isCalendarDay = (valueArg) => {
9
+ if (typeof valueArg !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(valueArg)) {
10
+ return false;
11
+ }
12
+ const [year, month, day] = valueArg.split('-').map(Number);
13
+ const date = new Date(Date.UTC(year, month - 1, day));
14
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
15
+ };
16
+ const assertReceipt = (receiptArg, indexArg) => {
17
+ if (!isCalendarDay(receiptArg.receivedOn)) {
18
+ throw new Error(`receipts[${indexArg}].receivedOn must be a calendar day YYYY-MM-DD, got ${String(receiptArg.receivedOn)}`);
19
+ }
20
+ if (!Number.isSafeInteger(receiptArg.entgeltCents)) {
21
+ throw new Error(`receipts[${indexArg}].entgeltCents must be whole cents, got ${String(receiptArg.entgeltCents)}`);
22
+ }
23
+ if (!GESAMTUMSATZ_PARTS.includes(receiptArg.part)) {
24
+ throw new Error(`receipts[${indexArg}].part must be one of ${GESAMTUMSATZ_PARTS.join(', ')}, got ${String(receiptArg.part)}`);
25
+ }
26
+ };
27
+ const assertYear = (yearArg) => {
28
+ if (!Number.isSafeInteger(yearArg) || yearArg < 1900 || yearArg > 9999) {
29
+ throw new Error(`year must be a calendar year, got ${String(yearArg)}`);
30
+ }
31
+ };
32
+ /** The Gesamtumsatz of a calendar year (§ 19 Abs. 2 UStG): the Entgelte received in it that count, and those left out. */
33
+ export function gesamtumsatzOf(receiptsArg, yearArg) {
34
+ assertYear(yearArg);
35
+ const leftOutCents = { exempt_section_4_listed: 0, exempt_ancillary: 0, fixed_assets: 0, not_taxable_here: 0 };
36
+ let totalCents = 0;
37
+ receiptsArg.forEach((receiptArg, indexArg) => {
38
+ assertReceipt(receiptArg, indexArg);
39
+ if (Number(receiptArg.receivedOn.slice(0, 4)) !== yearArg) {
40
+ return;
41
+ }
42
+ if (receiptArg.part === 'counted') {
43
+ totalCents += receiptArg.entgeltCents;
44
+ }
45
+ else {
46
+ leftOutCents[receiptArg.part] += receiptArg.entgeltCents;
47
+ }
48
+ });
49
+ return { year: yearArg, totalCents, leftOutCents };
50
+ }
51
+ /**
52
+ * Whether a calendar year's supplies are exempt under § 19 Abs. 1 UStG: from
53
+ * its start where the previous year's Gesamtumsatz did not exceed 25 000 € and
54
+ * no waiver is in force, until the receipt by which the running year's
55
+ * Gesamtumsatz exceeds 100 000 € (`endsWith`).
56
+ */
57
+ export function smallBusinessStatusOf(inputArg) {
58
+ assertYear(inputArg.year);
59
+ const previous = inputArg.previousYearCents;
60
+ if (previous !== undefined && previous !== 'business_started' && (!Number.isSafeInteger(previous) || previous < 0)) {
61
+ throw new Error(`previousYearCents must be whole cents of at least 0, 'business_started' or undefined, got ${String(previous)}`);
62
+ }
63
+ const counted = inputArg.receipts
64
+ .map((receiptArg, indexArg) => {
65
+ assertReceipt(receiptArg, indexArg);
66
+ return { receipt: receiptArg, index: indexArg };
67
+ })
68
+ .filter(({ receipt }) => Number(receipt.receivedOn.slice(0, 4)) === inputArg.year && receipt.part === 'counted')
69
+ // by day; receipts of one day in the order given
70
+ .sort((leftArg, rightArg) => leftArg.receipt.receivedOn.localeCompare(rightArg.receipt.receivedOn) || leftArg.index - rightArg.index);
71
+ const runningCents = counted.reduce((sumArg, { receipt }) => sumArg + receipt.entgeltCents, 0);
72
+ const base = { year: inputArg.year, runningCents };
73
+ if (inputArg.year < SMALL_BUSINESS_FIRST_YEAR) {
74
+ return { ...base, atStart: 'unknown', reason: 'before_2025' };
75
+ }
76
+ if (inputArg.waived === true) {
77
+ return { ...base, atStart: 'taxable', reason: 'waived' };
78
+ }
79
+ if (previous === 'business_started') {
80
+ return { ...base, atStart: 'unknown', reason: 'business_started_this_year' };
81
+ }
82
+ if (previous === undefined) {
83
+ return { ...base, atStart: 'unknown', reason: 'previous_year_unknown' };
84
+ }
85
+ if (previous > SMALL_BUSINESS_PREVIOUS_YEAR_LIMIT_CENTS) {
86
+ return { ...base, atStart: 'taxable', reason: 'previous_year_over' };
87
+ }
88
+ let sum = 0;
89
+ for (const { receipt, index } of counted) {
90
+ sum += receipt.entgeltCents;
91
+ if (sum > SMALL_BUSINESS_RUNNING_YEAR_LIMIT_CENTS) {
92
+ return { ...base, atStart: 'exempt', reason: 'within_limits', endsWith: { receivedOn: receipt.receivedOn, index, runningCents: sum } };
93
+ }
94
+ }
95
+ return { ...base, atStart: 'exempt', reason: 'within_limits' };
96
+ }
97
+ /** The last calendar year a waiver effective for `yearArg` binds (§ 19 Abs. 3 Satz 3 UStG: at least five calendar years). */
98
+ export function smallBusinessWaiverBindsUntil(yearArg) {
99
+ assertYear(yearArg);
100
+ return yearArg + 4;
101
+ }
102
+ /** The first calendar year from whose start a waiver effective for `yearArg` can be revoked (§ 19 Abs. 3 Satz 3 and 4 UStG). */
103
+ export function smallBusinessWaiverRevocableFrom(yearArg) {
104
+ return smallBusinessWaiverBindsUntil(yearArg) + 1;
105
+ }
106
+ /** The last day a waiver for the Besteuerungszeitraum `yearArg` can be declared: the last day of February two years later (§ 19 Abs. 3 Satz 1 UStG). */
107
+ export function smallBusinessWaiverDeadline(yearArg) {
108
+ assertYear(yearArg);
109
+ const year = yearArg + 2;
110
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
111
+ return `${year}-02-${leap ? '29' : '28'}`;
112
+ }
113
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hbGxidXNpbmVzcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYWxsYnVzaW5lc3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBMkNBLDhIQUE4SDtBQUM5SCxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyxJQUFJLENBQUM7QUFFOUMsNkVBQTZFO0FBQzdFLE1BQU0sQ0FBQyxNQUFNLHdDQUF3QyxHQUFXLFNBQVMsQ0FBQztBQUMxRSw2RUFBNkU7QUFDN0UsTUFBTSxDQUFDLE1BQU0sdUNBQXVDLEdBQVcsVUFBVSxDQUFDO0FBa0IxRSxNQUFNLENBQUMsTUFBTSxrQkFBa0IsR0FBaUMsQ0FBQyxTQUFTLEVBQUUseUJBQXlCLEVBQUUsa0JBQWtCLEVBQUUsY0FBYyxFQUFFLGtCQUFrQixDQUFDLENBQUM7QUEwRC9KLE1BQU0sYUFBYSxHQUFHLENBQUMsUUFBaUIsRUFBc0IsRUFBRTtJQUM5RCxJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxDQUFDLHFCQUFxQixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1FBQzFFLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE1BQU0sQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsQ0FBQyxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQzNELE1BQU0sSUFBSSxHQUFHLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssR0FBRyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN0RCxPQUFPLElBQUksQ0FBQyxjQUFjLEVBQUUsS0FBSyxJQUFJLElBQUksSUFBSSxDQUFDLFdBQVcsRUFBRSxLQUFLLEtBQUssR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsRUFBRSxLQUFLLEdBQUcsQ0FBQztBQUN6RyxDQUFDLENBQUM7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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/tax",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@git.zone/tsbuild": "^5.0.0",
36
- "@git.zone/tsrun": "^3.0.1",
36
+ "@git.zone/tsrun": "^3.0.2",
37
37
  "@git.zone/tstest": "^6.3.2",
38
38
  "@types/node": "^26.6.2"
39
39
  },
package/readme.md CHANGED
@@ -73,6 +73,32 @@ Set `taxationMethod` so the result records which mode the figures represent. If
73
73
 
74
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
75
 
76
+ ## Kleinunternehmer (§ 19 UStG)
77
+
78
+ Since 2025 a Kleinunternehmer's supplies are tax-exempt where the Gesamtumsatz
79
+ did not exceed 25 000 € in the previous calendar year and does not exceed
80
+ 100 000 € in the running one (§ 19 Abs. 1 Satz 1 UStG). The Gesamtumsatz counts
81
+ the Entgelte received (§ 19 Abs. 2), less the listed § 4 exemptions, the
82
+ Hilfsumsätze and fixed assets.
83
+
84
+ ```typescript
85
+ const status = tax.smallBusinessStatusOf({
86
+ year: 2026,
87
+ previousYearCents: tax.gesamtumsatzOf(receipts, 2025).totalCents,
88
+ receipts, // [{ receivedOn: '2026-03-01', entgeltCents: 120000, part: 'counted' }, …]
89
+ });
90
+ // { atStart: 'exempt', reason: 'within_limits', endsWith?: { receivedOn, index, runningCents }, runningCents }
91
+ ```
92
+
93
+ `endsWith` names the receipt by which the running year first exceeds
94
+ 100 000 €; it and every later one are taxed. A waiver (§ 19 Abs. 3) makes the
95
+ year `taxable`; `smallBusinessWaiverDeadline`, `…BindsUntil` and
96
+ `…RevocableFrom` give its dates. The year the business starts answers
97
+ `unknown`: the statute names no limit for it. So does a year before 2025
98
+ (`before_2025`), under the version with other limits (22 000 € / 50 000 €,
99
+ with the tax). The rules cover businesses resident in Germany or the areas of
100
+ § 1 Abs. 3 UStG; the new vehicles of § 19 Abs. 6 are not modelled.
101
+
76
102
  ## German VAT rates, dated
77
103
 
78
104
  `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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/tax',
6
- version: '2.2.0',
6
+ version: '2.3.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
@@ -3,3 +3,4 @@ export * from './ustva.accounts.js';
3
3
  export * from './ustva.js';
4
4
  export * from './zm.js';
5
5
  export * from './rates.js';
6
+ export * from './smallbusiness.js';
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The Kleinunternehmer of § 19 UStG in the version from 1 January 2025 and
3
+ * its Gesamtumsatz. Pure; the caller states each Entgelt received and what
4
+ * kind of supply it paid, and records the waiver.
5
+ *
6
+ * Source: § 19 UStG on gesetze-im-internet.de, read 2026-09-29.
7
+ * - Abs. 1 Satz 1: a supply (§ 1 Abs. 1 Nr. 1) of a business resident in
8
+ * Germany or in the areas of § 1 Abs. 3 UStG "ist steuerfrei, wenn der Gesamtumsatz nach Absatz 2 im
9
+ * vorangegangenen Kalenderjahr 25 000 Euro nicht überschritten hat und im
10
+ * laufenden Kalenderjahr 100 000 Euro nicht überschreitet".
11
+ * - Abs. 2: "Gesamtumsatz ist die nach vereinnahmten Entgelten berechnete
12
+ * Summe der vom Unternehmer ausgeführten steuerbaren Umsätze im Sinne des
13
+ * § 1 Absatz 1 Nummer 1 abzüglich folgender Umsätze: 1. Umsätze, die nach
14
+ * § 4 Nummer 8 Buchstabe i, Nummer 9 Buchstabe b und Nummer 11 bis 29
15
+ * steuerfrei sind; 2. Umsätze, die nach § 4 Nummer 8 Buchstabe a bis h,
16
+ * Nummer 9 Buchstabe a und Nummer 10 steuerfrei sind, wenn sie Hilfsumsätze
17
+ * sind. Die Umsätze mit Wirtschaftsgütern des Anlagevermögens eines
18
+ * Unternehmers bleiben bei der Ermittlung des Gesamtumsatzes nach Satz 1
19
+ * außer Ansatz."
20
+ * - Abs. 3: the waiver, declared "bis zum letzten Tag des Monats Februar des
21
+ * zweiten auf den Besteuerungszeitraum folgenden Kalenderjahres",
22
+ * effective from the start of the Besteuerungszeitraum it is for, binding
23
+ * "mindestens für fünf Kalenderjahre", revocable "mit Wirkung von Beginn
24
+ * eines darauffolgenden Kalenderjahres".
25
+ *
26
+ * - Abs. 6: not for the intra-Community supply of new vehicles; not modelled
27
+ * here (the caller counts such a supply taxed).
28
+ * - The version until 31 December 2024 had other limits (22 000 € in the
29
+ * previous year, 50 000 € expected in the running one, both with the
30
+ * tax) and did not exempt the supplies but levied no tax on them; a year
31
+ * before 2025 answers `before_2025` and is not told.
32
+ *
33
+ * Not read: the BMF-Schreiben of 18 March 2025 (UStAE Abschn. 19.1 ff.),
34
+ * which the BMF site did not serve. Two questions it answers stay open here:
35
+ * the limit in the year the business starts, which the statute does not name
36
+ * (`business_started_this_year` answers `unknown`), and whether the supply by
37
+ * which the running year exceeds 100 000 € is itself taxed. The statute's
38
+ * wording (the running year's Gesamtumsatz "nicht überschreitet") is read so
39
+ * that it is: `endsWith` names the receipt at which the sum first exceeds the
40
+ * limit, and every receipt from it on is taxed.
41
+ */
42
+ import type { TCents } from '@fin.cx/calculation';
43
+
44
+ /** The first calendar year of the version of § 19 UStG this module applies (Jahressteuergesetz 2024, from 1 January 2025). */
45
+ export const SMALL_BUSINESS_FIRST_YEAR = 2025;
46
+
47
+ /** 25 000 €: the previous calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
48
+ export const SMALL_BUSINESS_PREVIOUS_YEAR_LIMIT_CENTS: TCents = 2_500_000;
49
+ /** 100 000 €: the running calendar year's limit (§ 19 Abs. 1 Satz 1 UStG) */
50
+ export const SMALL_BUSINESS_RUNNING_YEAR_LIMIT_CENTS: TCents = 10_000_000;
51
+
52
+ /**
53
+ * What a receipt is to the Gesamtumsatz (§ 19 Abs. 2 UStG):
54
+ * - `counted`: an Entgelt for a steuerbarer Umsatz (§ 1 Abs. 1 Nr. 1), taxed or
55
+ * exempt otherwise than below, a Kleinunternehmer's own exempt supplies
56
+ * included;
57
+ * - `exempt_section_4_listed`: exempt under § 4 Nr. 8 Buchst. i, Nr. 9
58
+ * Buchst. b or Nr. 11 bis 29 (Satz 1 Nr. 1), left out;
59
+ * - `exempt_ancillary`: exempt under § 4 Nr. 8 Buchst. a bis h, Nr. 9 Buchst.
60
+ * a or Nr. 10 and a Hilfsumsatz (Satz 1 Nr. 2), left out; such a supply that
61
+ * is no Hilfsumsatz is `counted`;
62
+ * - `fixed_assets`: a sale of fixed assets (Satz 2), left out;
63
+ * - `not_taxable_here`: no steuerbarer Umsatz in Germany (its place of supply
64
+ * abroad), left out.
65
+ */
66
+ export type TGesamtumsatzPart = 'counted' | 'exempt_section_4_listed' | 'exempt_ancillary' | 'fixed_assets' | 'not_taxable_here';
67
+
68
+ export const GESAMTUMSATZ_PARTS: readonly TGesamtumsatzPart[] = ['counted', 'exempt_section_4_listed', 'exempt_ancillary', 'fixed_assets', 'not_taxable_here'];
69
+
70
+ /** One Entgelt received (§ 19 Abs. 2 UStG counts by receipts), or a refund of one (negative). */
71
+ export interface IGesamtumsatzReceipt {
72
+ /** the day it was received, `YYYY-MM-DD` */
73
+ receivedOn: string;
74
+ /** the Entgelt, whole cents, without the tax where one is owed on it; negative for a refund */
75
+ entgeltCents: TCents;
76
+ part: TGesamtumsatzPart;
77
+ }
78
+
79
+ /** The Gesamtumsatz of a calendar year, and what § 19 Abs. 2 left out of it. */
80
+ export interface IGesamtumsatzOfYear {
81
+ year: number;
82
+ totalCents: TCents;
83
+ leftOutCents: Record<Exclude<TGesamtumsatzPart, 'counted'>, TCents>;
84
+ }
85
+
86
+ /** Why a year starts with the exemption or without it. */
87
+ export type TSmallBusinessReason =
88
+ | 'within_limits'
89
+ | 'previous_year_over'
90
+ | 'waived'
91
+ | 'previous_year_unknown'
92
+ | 'business_started_this_year'
93
+ /** a year before 2025, under the version with other limits (`SMALL_BUSINESS_FIRST_YEAR`): not told */
94
+ | 'before_2025';
95
+
96
+ export interface ISmallBusinessYearInput {
97
+ /** the calendar year (the Besteuerungszeitraum, § 16 Abs. 1 Satz 2 UStG) */
98
+ year: number;
99
+ /**
100
+ * the Gesamtumsatz of the previous calendar year, whole cents; `undefined`
101
+ * where it is not known; `'business_started'` where the business started in
102
+ * this year and had no previous year
103
+ */
104
+ previousYearCents: TCents | undefined | 'business_started';
105
+ /** a waiver (§ 19 Abs. 3 UStG) is in force for the year */
106
+ waived?: boolean;
107
+ /** the receipts of the year, in any order; those of other years are ignored */
108
+ receipts: readonly IGesamtumsatzReceipt[];
109
+ }
110
+
111
+ export interface ISmallBusinessStatus {
112
+ year: number;
113
+ /** whether the year's supplies are exempt from its start; `unknown` where the input cannot tell */
114
+ atStart: 'exempt' | 'taxable' | 'unknown';
115
+ reason: TSmallBusinessReason;
116
+ /**
117
+ * Where the year starts exempt and its Gesamtumsatz exceeds 100 000 €: the
118
+ * receipt by which it first does (its day, its place in the receipts as
119
+ * given, the sum with it). It and every later receipt are taxed.
120
+ */
121
+ endsWith?: { receivedOn: string; index: number; runningCents: TCents };
122
+ /** the year's Gesamtumsatz over all its receipts */
123
+ runningCents: TCents;
124
+ }
125
+
126
+ const isCalendarDay = (valueArg: unknown): valueArg is string => {
127
+ if (typeof valueArg !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(valueArg)) {
128
+ return false;
129
+ }
130
+ const [year, month, day] = valueArg.split('-').map(Number);
131
+ const date = new Date(Date.UTC(year, month - 1, day));
132
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
133
+ };
134
+
135
+ const assertReceipt = (receiptArg: IGesamtumsatzReceipt, indexArg: number): void => {
136
+ if (!isCalendarDay(receiptArg.receivedOn)) {
137
+ throw new Error(`receipts[${indexArg}].receivedOn must be a calendar day YYYY-MM-DD, got ${String(receiptArg.receivedOn)}`);
138
+ }
139
+ if (!Number.isSafeInteger(receiptArg.entgeltCents)) {
140
+ throw new Error(`receipts[${indexArg}].entgeltCents must be whole cents, got ${String(receiptArg.entgeltCents)}`);
141
+ }
142
+ if (!GESAMTUMSATZ_PARTS.includes(receiptArg.part)) {
143
+ throw new Error(`receipts[${indexArg}].part must be one of ${GESAMTUMSATZ_PARTS.join(', ')}, got ${String(receiptArg.part)}`);
144
+ }
145
+ };
146
+
147
+ const assertYear = (yearArg: number): void => {
148
+ if (!Number.isSafeInteger(yearArg) || yearArg < 1900 || yearArg > 9999) {
149
+ throw new Error(`year must be a calendar year, got ${String(yearArg)}`);
150
+ }
151
+ };
152
+
153
+ /** The Gesamtumsatz of a calendar year (§ 19 Abs. 2 UStG): the Entgelte received in it that count, and those left out. */
154
+ export function gesamtumsatzOf(receiptsArg: readonly IGesamtumsatzReceipt[], yearArg: number): IGesamtumsatzOfYear {
155
+ assertYear(yearArg);
156
+ const leftOutCents: IGesamtumsatzOfYear['leftOutCents'] = { exempt_section_4_listed: 0, exempt_ancillary: 0, fixed_assets: 0, not_taxable_here: 0 };
157
+ let totalCents = 0;
158
+ receiptsArg.forEach((receiptArg, indexArg) => {
159
+ assertReceipt(receiptArg, indexArg);
160
+ if (Number(receiptArg.receivedOn.slice(0, 4)) !== yearArg) {
161
+ return;
162
+ }
163
+ if (receiptArg.part === 'counted') {
164
+ totalCents += receiptArg.entgeltCents;
165
+ } else {
166
+ leftOutCents[receiptArg.part] += receiptArg.entgeltCents;
167
+ }
168
+ });
169
+ return { year: yearArg, totalCents, leftOutCents };
170
+ }
171
+
172
+ /**
173
+ * Whether a calendar year's supplies are exempt under § 19 Abs. 1 UStG: from
174
+ * its start where the previous year's Gesamtumsatz did not exceed 25 000 € and
175
+ * no waiver is in force, until the receipt by which the running year's
176
+ * Gesamtumsatz exceeds 100 000 € (`endsWith`).
177
+ */
178
+ export function smallBusinessStatusOf(inputArg: ISmallBusinessYearInput): ISmallBusinessStatus {
179
+ assertYear(inputArg.year);
180
+ const previous = inputArg.previousYearCents;
181
+ if (previous !== undefined && previous !== 'business_started' && (!Number.isSafeInteger(previous) || previous < 0)) {
182
+ throw new Error(`previousYearCents must be whole cents of at least 0, 'business_started' or undefined, got ${String(previous)}`);
183
+ }
184
+ const counted = inputArg.receipts
185
+ .map((receiptArg, indexArg) => {
186
+ assertReceipt(receiptArg, indexArg);
187
+ return { receipt: receiptArg, index: indexArg };
188
+ })
189
+ .filter(({ receipt }) => Number(receipt.receivedOn.slice(0, 4)) === inputArg.year && receipt.part === 'counted')
190
+ // by day; receipts of one day in the order given
191
+ .sort((leftArg, rightArg) => leftArg.receipt.receivedOn.localeCompare(rightArg.receipt.receivedOn) || leftArg.index - rightArg.index);
192
+ const runningCents = counted.reduce((sumArg, { receipt }) => sumArg + receipt.entgeltCents, 0);
193
+ const base = { year: inputArg.year, runningCents };
194
+ if (inputArg.year < SMALL_BUSINESS_FIRST_YEAR) {
195
+ return { ...base, atStart: 'unknown', reason: 'before_2025' };
196
+ }
197
+ if (inputArg.waived === true) {
198
+ return { ...base, atStart: 'taxable', reason: 'waived' };
199
+ }
200
+ if (previous === 'business_started') {
201
+ return { ...base, atStart: 'unknown', reason: 'business_started_this_year' };
202
+ }
203
+ if (previous === undefined) {
204
+ return { ...base, atStart: 'unknown', reason: 'previous_year_unknown' };
205
+ }
206
+ if (previous > SMALL_BUSINESS_PREVIOUS_YEAR_LIMIT_CENTS) {
207
+ return { ...base, atStart: 'taxable', reason: 'previous_year_over' };
208
+ }
209
+ let sum = 0;
210
+ for (const { receipt, index } of counted) {
211
+ sum += receipt.entgeltCents;
212
+ if (sum > SMALL_BUSINESS_RUNNING_YEAR_LIMIT_CENTS) {
213
+ return { ...base, atStart: 'exempt', reason: 'within_limits', endsWith: { receivedOn: receipt.receivedOn, index, runningCents: sum } };
214
+ }
215
+ }
216
+ return { ...base, atStart: 'exempt', reason: 'within_limits' };
217
+ }
218
+
219
+ /** The last calendar year a waiver effective for `yearArg` binds (§ 19 Abs. 3 Satz 3 UStG: at least five calendar years). */
220
+ export function smallBusinessWaiverBindsUntil(yearArg: number): number {
221
+ assertYear(yearArg);
222
+ return yearArg + 4;
223
+ }
224
+
225
+ /** The first calendar year from whose start a waiver effective for `yearArg` can be revoked (§ 19 Abs. 3 Satz 3 and 4 UStG). */
226
+ export function smallBusinessWaiverRevocableFrom(yearArg: number): number {
227
+ return smallBusinessWaiverBindsUntil(yearArg) + 1;
228
+ }
229
+
230
+ /** The last day a waiver for the Besteuerungszeitraum `yearArg` can be declared: the last day of February two years later (§ 19 Abs. 3 Satz 1 UStG). */
231
+ export function smallBusinessWaiverDeadline(yearArg: number): string {
232
+ assertYear(yearArg);
233
+ const year = yearArg + 2;
234
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
235
+ return `${year}-02-${leap ? '29' : '28'}`;
236
+ }