@fin.cx/tax
German tax computation engine. The first module computes the UStVA (Umsatzsteuer-Voranmeldung) Kennzahlen from ledger account movements — pure functions, exact integer cents, loud failures.
Install
pnpm install @fin.cx/tax
What it does
computeUstva() turns one period's account movements into the UStVA figures:
-
Account→Kennzahl mapping comes from @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_ORDERlists the fields in the order of the UStVA 2026 form, with their labels inUSTVA_KENNZAHL_LABELS; a Kennzahl the chart data maps that has no place there is a filing issue, never dropped. -
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 × 19is an exact integer number of cents). -
Output-VAT accounts (
VAT_ACCOUNTS, overridable per call withvatAccounts) follow the DATEV Kontenrahmen 2026:SKR 03 SKR 04 Umsatzsteuer 19 % 1776 3806 Umsatzsteuer 7 % 1771 3801 Umsatzsteuer (undifferentiated, flagged) 1770 3800 Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 % 1774 3804 Umsatzsteuer nach § 13b UStG 19 % 1787 3837 igE of new vehicles from suppliers without a VAT ID (flagged) 1784 3834 -
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'staxScenario):reverseChargeServicesEuCentsfor § 13b Abs. 1 (Kz 46),reverseChargeRealEstateCentsfor the Umsätze under the GrEStG, § 13b Abs. 2 Nr. 3 (Kz 73),reverseChargeOtherCentsfor 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). -
Kz 69 (tax owed on other grounds), Zeile 47 of the USt 1 A 2026: "In Rechnungen unrichtig oder unberechtigt ausgewiesene Steuerbeträge (§ 14c UStG) sowie Steuerbeträge, die nach § 6a Absatz 4 Satz 2, § 17 Absatz 1 Satz 7, § 25b Absatz 2 UStG oder von einem Auslagerer oder Lagerhalter nach § 27 Absatz 40a UStG geschuldet werden" — e.g. the VAT a Kleinunternehmer's invoice states, or the tax the last buyer of a triangular transaction owes. The chart data maps no account to it, so the caller states it as
otherTaxOwed: { cents, onOutputAccountsCents? }; it is added to Kz 83, and the part booked on the domestic output-tax accounts (onOutputAccountsCents) is left out of the domestic reconciliation, which has no base for it in Kz 81 or 86. -
Kz 70 (the day of the change from the Kleinunternehmer scheme), Zeile 12 of the USt 1 A 2026: "Wechsel von der Kleinunternehmer-Regelung (§ 19 UStG) zur Regelbesteuerung – Datum des Wechsels". Per the Anleitung zur UStVA 2026 (p. 8) the day of the supply by which the running year's Gesamtumsatz exceeded 100 000 € (in the year the business started, 25 000 €), or 1 January for a waiver of § 19 Abs. 3 UStG from the running year; none where the previous year exceeded 25 000 €. The caller states it as
smallBusinessChangeOn(YYYY-MM-DD) in the return of the period that contains it; it comes back indateFields(IKennzahlDateValue:kennzahl,value,label), apart from the amounts. A day that is none, or one outside aperiodof the shapeYYYY-MM,YYYY-QnorYYYY, is an issue and is not returned. -
Kz 83 (Zahllast/Erstattung) is computed the way the form computes it, including an optional Sondervorauszahlung credit (Kz 39, Dauerfristverlängerung).
-
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.
-
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 whileissuesis non-empty.
Usage
import { computeUstva } from '@fin.cx/tax';
const result = computeUstva({
chart: 'SKR03',
year: 2026,
period: '2026-09',
// debitCents - creditCents per account, from your trial balance
movements: [
{ accountNumber: '8400', signedCents: -1_000_055 }, // 10 000.55 revenue 19 %
{ accountNumber: '1776', signedCents: -190_010 }, // output VAT ledger balance
{ accountNumber: '1576', signedCents: 123_456 }, // input VAT
],
// exact igE / §13b bases from tax-scenario-tagged entries (optional)
scenarioBases: { intraEuAcquisitionStandardCents: 200_000 },
});
if (result.issues.length > 0) throw new Error(result.issues.join('\n'));
for (const kz of result.kennzahlen) {
// kind 'baseEur' -> full euros; kind 'taxCents' -> cents
console.log(`Kz ${kz.kennzahl}: ${kz.value} (${kz.label})`);
}
console.log('Zahllast:', result.zahllastCents, 'cents');
console.log(result.reconciliation.checks.join('\n'));
Soll- vs. Ist-Versteuerung (§16 / §20 UStG)
This module aggregates whatever movements you pass in — it does not decide which entries belong to the period:
- Soll-Versteuerung (default assumption): pass movements selected by invoice/posting date.
- Ist-Versteuerung: pass movements selected by payment receipt.
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.
Zusammenfassende Meldung (§ 18a UStG)
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.
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.
Kleinunternehmer (§ 19 UStG)
Since 2025 a Kleinunternehmer's supplies are tax-exempt where the Gesamtumsatz did not exceed 25 000 € in the previous calendar year and does not exceed 100 000 € in the running one (§ 19 Abs. 1 Satz 1 UStG). The Gesamtumsatz counts the Entgelte received (§ 19 Abs. 2), less the listed § 4 exemptions, the Hilfsumsätze and fixed assets.
const status = tax.smallBusinessStatusOf({
year: 2026,
previousYearCents: tax.gesamtumsatzOf(receipts, 2025).totalCents,
receipts, // [{ receivedOn: '2026-03-01', entgeltCents: 120000, part: 'counted' }, …]
});
// { atStart: 'exempt', reason: 'within_limits', endsWith?: { receivedOn, index, runningCents }, runningCents }
endsWith names the receipt by which the running year first exceeds
100 000 €; it and every later one are taxed. A waiver (§ 19 Abs. 3) makes the
year taxable; smallBusinessWaiverDeadline, …BindsUntil and
…RevocableFrom give its dates. The year the business starts answers
unknown: the statute names no limit for it. So does a year before 2025
(before_2025), under the version with other limits (22 000 € / 50 000 €,
with the tax). The rules cover businesses resident in Germany or the areas of
§ 1 Abs. 3 UStG; the new vehicles of § 19 Abs. 6 are not modelled.
German VAT rates, dated
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.
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.
Scope
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.
License
MIT — see license.