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_25margin_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

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