Busflow Docs

Internal documentation portal

Skip to content

Tax Engine β€” Β§ 25 UStG Steuer-Engine ​

NestJS Domain Service for autonomous tax strategy resolution and compliance computation. Part of the Busflow Nebenbuch (Sub-Ledger) architecture.

Purpose ​

The Tax Engine is a NestJS Domain Service responsible for:

  1. Vorabkalkulation (Planning): Determining the correct tax_strategy on a CostingSheet based on its procurement_items composition
  2. Nachkalkulation (Actuals): Computing concrete tax amounts on TaxLedgerEntry records from realized FinancialLedger data
  3. Invoice Generation: Applying the resolved tax strategy to produce legally compliant invoice line items

Tax Strategy Resolution Logic ​

The engine classifies each tour/charter based on the presence of third-party procurement:

IF all CostComponent.service_type == "EIGEN"
  β†’ STANDARD_VAT (Regelbesteuerung, 19%)
  β†’ Tax base = full net selling price

IF any CostComponent.service_type == "FREMD"
  β†’ MARGIN_SCHEME_25 (Β§ 25 UStG Margenbesteuerung)
  β†’ Tax base = gross customer price βˆ’ gross procurement costs (margin only)
  β†’ Input VAT deduction on Fremdleistungen is forfeited (Β§ 25 Abs. 4)

Data Flow ​

mermaid
flowchart LR
    subgraph Backoffice
        CS[CostingSheet]
        PI["procurement_items<br/>JSONB array<br/>(incl. Geography)"]
    end

    subgraph Tax Engine
        TR[TaxResolver<br/>Domain Service]
        TC[TaxCalculator<br/>Domain Service]
    end

    subgraph Commerce
        FL[FinancialLedger]
        TLE["TaxLedgerEntry<br/>(Β§ 25 Abs. 5 fields)"]
        INV[Invoice]
    end

    CS --> PI
    PI -->|service_type EIGEN/FREMD<br/>+ geography EU/THIRD_COUNTRY| TR
    TR -->|tax_strategy| CS

    FL -->|realized_revenue,<br/>realized_expense| TC
    CS -->|tax_strategy,<br/>procurement costs,<br/>geography| TC
    TC -->|Β§ 25 Abs. 5 fields:<br/>customer_gross, procurement_gross,<br/>margin_taxable, margin_exempt,<br/>tax_base, tax_amount, tax_rate| TLE
    TLE --> INV

Architecture Placement ​

ConcernLocationPattern
Tax strategy auto-resolutionapps/api/src/backoffice/services/tax-resolver.service.tsHasura Action handler
Tax computation on actualsapps/api/src/commerce/services/tax-calculator.service.tsHasura Action handler
Invoice tax line generationapps/api/src/commerce/services/invoice.service.tsHasura Action handler
CostingSheet mutation triggerHasura Event Trigger β†’ NestJS webhook@golevelup/nestjs-hasura decorator

Domain Events ​

EventEmitted ByConsumed By
CostingSheetUpdatedBackoffice (Hasura ET)Tax Engine: re-evaluates tax_strategy
FinancialLedgerClosedCommerce (FinancialLedgerService)Tax Engine: generates TaxLedgerEntry from FinancialLedger
TaxLedgerEntryCreatedCommerce (Tax Engine)DATEV Export Service
InvoiceIssuedCommerce (Invoice Service)Audit Trail (GOBD scope), Period Lock validation

Β§ 25 UStG β€” Implementation Notes ​

Β§ 25 Abs. 1 β€” Anwendungsbereich ​

Greift nur "soweit der Unternehmer Reisevorleistungen in Anspruch nimmt" β†’ triggers MARGIN_SCHEME_25 when any CostComponent.service_type == "FREMD".

Β§ 25 Abs. 2 β€” Drittlands-Steuerbefreiung ​

CAUTION

"Die sonstige Leistung ist steuerfrei, soweit die ihr zuzurechnenden Reisevorleistungen im Drittlandsgebiet bewirkt werden."

Jede CostComponent muss ein geography: EU | THIRD_COUNTRY Feld tragen. Die Tax Engine splittet die Marge in einen steuerpflichtigen EU-Anteil und einen steuerfreien Drittlands-Anteil.

Algorithmus:

procurement_eu = SUM(CostComponent WHERE geography == EU AND service_type == FREMD)
procurement_third = SUM(CostComponent WHERE geography == THIRD_COUNTRY AND service_type == FREMD)
procurement_total = procurement_eu + procurement_third

total_margin_gross = customer_gross_amount - procurement_gross_amount

IF total_margin_gross <= 0:
  margin_taxable_net = 0  // Negativmarge β†’ keine Steuer, kein Verlustvortrag
  margin_exempt_net = 0
  tax_amount = 0
ELSE:
  eu_ratio = procurement_eu / procurement_total
  third_ratio = procurement_third / procurement_total
  
  margin_eu_gross = total_margin_gross Γ— eu_ratio
  margin_third_gross = total_margin_gross Γ— third_ratio
  
  // Herausrechnen der USt fΓΌr die Bemessungsgrundlage (Netto-Marge)
  margin_taxable_net = margin_eu_gross / 1.19
  margin_exempt_net = margin_third_gross // Steuerfrei, Brutto = Netto
  
  tax_amount = margin_taxable_net Γ— 0.19

Β§ 25 Abs. 3 β€” Einzelmargenberechnung ​

Die Steuer bemisst sich nach der Differenz pro Reise (Einzelmarge), nicht aggregiert ΓΌber die Periode. Die TaxLedgerEntry-Berechnung erfolgt auf Ebene des einzelnen FinancialLedger.

Β§ 25 Abs. 4 β€” Vorsteuerabzugsverbot ​

WARNING

"Der Unternehmer ist nicht berechtigt, die ihm fΓΌr die Reisevorleistungen gesondert in Rechnung gestellten SteuerbetrΓ€ge als Vorsteuer abzuziehen."

Technische Konsequenz: Der datev-export.service.ts muss sicherstellen, dass Fremdleistungen (FREMD) auf spezielle DATEV-Konten ohne Vorsteuerabzug gemappt werden (z.B. SKR 03: Konto 3220 "Wareneingang Margenbesteuerung").

Β§ 25 Abs. 5 β€” Aufzeichnungspflichten ​

IMPORTANT

Aus den Aufzeichnungen mΓΌssen vier exakte Werte hervorgehen:

  1. customer_gross_amount β€” Der Betrag, den der LeistungsempfΓ€nger aufwendet
  2. procurement_gross_amount β€” Die Aufwendungen fΓΌr Reisevorleistungen
  3. margin_taxable_net β€” Die Bemessungsgrundlage (steuerpflichtige Netto-Marge)
  4. margin_exempt_net β€” Die Aufteilung auf steuerpflichtige und steuerfreie Leistungen

Diese Werte mΓΌssen als unverΓ€nderliche DB-Spalten in commerce.tax_ledger_entries persistiert werden. On-the-fly Frontend-Berechnung ist nicht GoBD-konform.

Margin Calculation Formula ​

Brutto_Marge = Reisepreis (brutto) βˆ’ Reisevorleistungen (brutto)
Bemessungsgrundlage (Netto_Marge) = Brutto_Marge / (1 + taxRate)
USt auf Marge = Bemessungsgrundlage Γ— taxRate

Key rules:

  • Input VAT on third-party procurement (FREMD) cannot be deducted (Β§ 25 Abs. 4)
  • The system must calculate margin tax per booking, not aggregated across the period (Β§ 25 Abs. 3)
  • Mixed tours (EIGEN + FREMD components) β†’ entire tour falls under Β§ 25 UStG
  • Pure own services (charter transfer, no hotel/ferry) β†’ standard VAT with calculateStandardVat()
  • Negativmargen (Verlust-Tour): margin_taxable_net = 0, tax_amount = 0. Verluste dΓΌrfen nicht mit anderen Reisen verrechnet werden.

Edge Cases ​

ScenarioTax Treatment
Charter transfer (bus only, no hotel/ferry)STANDARD_VAT β€” pure Eigenleistung
Multi-day tour with hotel bookingMARGIN_SCHEME_25 β€” Fremdleistung detected
Tour with Swiss hotel (Drittland)MARGIN_SCHEME_25 β€” margin_exempt_net for Swiss portion
Mixed EU + Drittland procurementSplit: margin_taxable_net (EU ratio) + margin_exempt_net (third ratio)
Ancillaries (insurance, luggage)Follow the parent booking's strategy
OnboardSales (beverages, snacks)STANDARD_VAT β€” always Eigenleistung, tracked separately
Negative margin (loss-making tour)margin_taxable_net = 0, no tax due, but TaxLedgerEntry still required

Module Structure (Proposed) ​

apps/api/src/
β”œβ”€β”€ backoffice/
β”‚   └── services/
β”‚       └── tax-resolver.service.ts     # EIGEN/FREMD + Geography β†’ tax_strategy
β”œβ”€β”€ commerce/
β”‚   └── services/
β”‚       β”œβ”€β”€ tax-calculator.service.ts   # Actuals β†’ TaxLedgerEntry (Β§ 25 Abs. 5 fields)
β”‚       β”œβ”€β”€ invoice.service.ts          # TaxLedgerEntry β†’ Invoice
β”‚       └── period-lock.service.ts      # Immutability enforcement
└── shared/
    └── tax/
        β”œβ”€β”€ tax-strategy.enum.ts        # STANDARD_VAT | MARGIN_SCHEME_25
        β”œβ”€β”€ geography.enum.ts           # EU | THIRD_COUNTRY
        β”œβ”€β”€ schemas.ts                  # Valibot: CostComponent, TaxRule, MarginResult
        β”œβ”€β”€ margin-calculator.ts        # Β§ 25 UStG: dynamic rate, split, Negativmarge
        └── vat-calculator.ts           # Standard VAT: configurable rate

Validation Rules (Valibot) ​

Tax-relevant data must be validated at the domain boundary using Valibot schemas:

  • CostComponent.service_type must be "EIGEN" or "FREMD"
  • CostComponent.geography must be "EU" or "THIRD_COUNTRY" (required when service_type == "FREMD")
  • CostComponent.tax_rule.rate must match the resolved strategy
  • TaxLedgerEntry.margin_taxable_net must be >= 0 (Negativmarge β†’ clamp to 0)
  • TaxLedgerEntry.margin_exempt_net must be >= 0
  • TaxLedgerEntry.customer_gross_amount and procurement_gross_amount must be > 0

Dependencies ​

  • Existing: CostingSheet schema, FinancialLedger schema, TaxLedgerEntry schema (extended), Invoice schema
  • New: commerce.change_events, commerce.ledger_period_locks
  • External: None β€” tax calculation is fully internal. DATEV export consumes the output but is a separate service.

Internal documentation β€” Busflow