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:
- Vorabkalkulation (Planning): Determining the correct
tax_strategyon aCostingSheetbased on itsprocurement_itemscomposition - Nachkalkulation (Actuals): Computing concrete tax amounts on
TaxLedgerEntryrecords from realizedFinancialLedgerdata - 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
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 --> INVArchitecture Placement
| Concern | Location | Pattern |
|---|---|---|
| Tax strategy auto-resolution | apps/api/src/backoffice/services/tax-resolver.service.ts | Hasura Action handler |
| Tax computation on actuals | apps/api/src/commerce/services/tax-calculator.service.ts | Hasura Action handler |
| Invoice tax line generation | apps/api/src/commerce/services/invoice.service.ts | Hasura Action handler |
| CostingSheet mutation trigger | Hasura Event Trigger → NestJS webhook | @golevelup/nestjs-hasura decorator |
Domain Events
| Event | Emitted By | Consumed By |
|---|---|---|
CostingSheetUpdated | Backoffice (Hasura ET) | Tax Engine: re-evaluates tax_strategy |
FinancialLedgerClosed | Commerce (FinancialLedgerService) | Tax Engine: generates TaxLedgerEntry from FinancialLedger |
TaxLedgerEntryCreated | Commerce (Tax Engine) | DATEV Export Service |
InvoiceIssued | Commerce (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:
customer_gross_amount— Der Betrag, den der Leistungsempfänger aufwendetprocurement_gross_amount— Die Aufwendungen für Reisevorleistungenmargin_taxable_net— Die Bemessungsgrundlage (steuerpflichtige Netto-Marge)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 × taxRateKey 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
| Scenario | Tax Treatment |
|---|---|
| Charter transfer (bus only, no hotel/ferry) | STANDARD_VAT — pure Eigenleistung |
| Multi-day tour with hotel booking | MARGIN_SCHEME_25 — Fremdleistung detected |
| Tour with Swiss hotel (Drittland) | MARGIN_SCHEME_25 — margin_exempt_net for Swiss portion |
| Mixed EU + Drittland procurement | Split: 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_typemust be"EIGEN"or"FREMD"CostComponent.geographymust be"EU"or"THIRD_COUNTRY"(required whenservice_type == "FREMD")CostComponent.tax_rule.ratemust match the resolved strategyTaxLedgerEntry.margin_taxable_netmust be>= 0(Negativmarge → clamp to 0)TaxLedgerEntry.margin_exempt_netmust be>= 0TaxLedgerEntry.customer_gross_amountandprocurement_gross_amountmust be> 0
Dependencies
- Existing:
CostingSheetschema,FinancialLedgerschema,TaxLedgerEntryschema (extended),Invoiceschema - 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.