@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, 41, 43, 48, 60); Vorsteuer accounts feed the tax fields (Kz 66, 61, 67, 62) cent-exact.
- 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). - 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'staxScenario); without them the module derives the 19 % bases from the output-tax account movements and marks them(derived). - 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, and undifferentiated output-VAT balances 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.
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.