Skip to content

feat(v0.11): Steuer-Rücklage, Danger Zone, Storno-Fixes#68

Merged
jonax1337 merged 16 commits into
mainfrom
release/v0.11
May 19, 2026
Merged

feat(v0.11): Steuer-Rücklage, Danger Zone, Storno-Fixes#68
jonax1337 merged 16 commits into
mainfrom
release/v0.11

Conversation

@jonax1337
Copy link
Copy Markdown
Owner

Summary

v0.11.0 — fokussiert auf Steuer-Rücklage mit ehrlicher § 32a-EStG-Berechnung (Tarif 2024/2025/2026 hartcodiert, voll getestet), plus aufgesammelte Fixes & QoL.

Feature-Brocken

  • Steuerprofil in Settings (Rechtsform, Hebesatz, KiSt, Familienstand, Quartals-Vorauszahlungen). Steuerprofil in Settings (Rechtsform, Hebesatz, KiSt, Vorauszahlungen) #64
  • Tarif-Library in src/lib/tax/ mit § 32a EStG (5 Zonen, Splittingtarif), Soli (Milderungszone), KiSt, GewSt (Freibetrag, Anrechnung § 35) — 38 Vitest-Tests gegen amtliche BMF-Eckpunkte für VZ 2024, 2025, 2026. Tarif-Library: ESt + Soli + KiSt + GewSt mit Vitest-Tests #65
  • Dashboard-Card + /reports/taxes-Route mit Jahres-Selector (2024–2027), Year-aware: Steuerjahr leitet sich automatisch aus der Dashboard-Periode ab. Dashboard-Card Steuer-Rücklage + Detail-Route /reports/taxes #66
  • Pauschal-Modus optional (Slider 0–50 %), zeigt Detail-Tarif vs. Pauschal-Daumenregel nebeneinander. Pauschal-Modus für Steuer-Rücklage (optional) #67
  • Multi-Year Vorauszahlungen als separate tax_prepayments-Tabelle (statt fixer Q1–Q4-Spalten an settings).
  • Sonstige Einkünfte (Brutto-Lohn Nebenberuf) → marginale ESt-Berechnung ESt(other+selbst) − ESt(other). Behebt den Nebenberufler-Bug, bei dem die Selbst-Einnahmen sonst im falschen Grenzsteuersatz lagen.
  • YTD vs. Hochgerechnet als zwei klickbare Cards. Eine einzelne Aprilrechnung führt nicht mehr zu absurden Jahresprojektionen — der YTD-Modus zeigt was bisher an Steuerlast ausgelöst wurde.
  • Danger Zone in Settings (3 Aktionen: alle Geschäftsdaten / einzelne Tabellen / Counter-Reset). Bestätigung per Tippen von LÖSCHEN, Auto-Backup vor jedem Wipe (Documents/Zettel/Backups/ mit App-Data-Fallback).

Bug-Fixes

  • Storno-eurTotalCent positiv gespeichert → Dashboard-Aggregate ($COALESCE(eur_total_cent, total)$) addierten Stornos doppelt statt rauszunetten. Saldo wurde so um den 2× Storno-Betrag zu hoch angezeigt. createInvoice/updateInvoice setzen jetzt das gleiche sign-Vorzeichen für eurTotalCent wie für subtotal/total. Migration 0018 repariert Bestandsdaten.
  • Storno-Aggregation in der Steuer-Rücklage zog die separat aufsummierten (negativen) Stornos vom Gewinn ab → addiert den Betrag doppelt statt abzuziehen. Auf SUM(subtotal) mit natürlichem Vorzeichen umgestellt.
  • Draft-Ausgaben im Dashboard-Saldo zählten mit (Expense-Query hatte keinen Status-Filter). Auf status IN ('open','paid') gezogen.
  • „ggü. Vorjahr" entfernt (Display + dead code), die kleine ±%-Zeile war mehr Rauschen als Signal.
  • Calculator-Icon raus aus TaxReport-Header und Dashboard-Steuer-Card.
  • BMF-Live-API verworfen — der Lohnsteuer-Rechner-Endpoint braucht einen registrierten Zugriffscode, der außer für Lohnabrechnungs-Software-Hersteller nicht erteilt wird. Statt Halb-Funktioniert-Halb-Nicht: drei Tarifjahre amtlich hartcodiert + Update-Pfad dokumentiert.

Migrationen

  • 0014_v0.11_tax_profile.sql — Steuerprofil-Spalten an settings
  • 0015_v0.11_pauschal_mode.sqlpauschal_tax_percent + use_pauschal_tax_reserve
  • 0016_v0.11_tax_prepayments.sql — Multi-Year Tabelle, Migration der Bestands-Q1-Q4 ins neue Format
  • 0017_v0.11_other_income.sqlother_income_annual_cent
  • 0018_v0.11_fix_storno_eur_total.sql — Vorzeichen-Repair für bestehende Stornos

Schema-Trias (lib.rs / backup.rs / Settings.svelte) auf 19.

Closes

Test plan

  • Settings → Steuerprofil korrekt persistent (legalForm, churchRate, filingStatus, otherIncomeAnnual, pauschalPercent)
  • Dashboard zeigt Steuer-Rücklage-Card mit YTD + Hochgerechnet nebeneinander
  • Jahres-Wechsel im Dashboard-Period-Selector aktualisiert die Steuer-Karte
  • /reports/taxes → Year-Dropdown lädt verschiedene Jahre
  • Storno-Rechnung erstellen + bezahlt-markieren → Saldo geht korrekt zurück
  • Migration 0018 hat bestehende kaputte Stornos repariert (Saldo stimmt nach App-Start)
  • Danger Zone: „Counter zurücksetzen" → Toast, Counter im Settings auf 0
  • Danger Zone: „Einzelne Tabellen leeren" → Auto-Backup unter Documents/Zettel/Backups oder AppData/digital.laux.zettel/Backups
  • Danger Zone: LÖSCHEN muss exakt getippt sein, Button bleibt sonst disabled

🤖 Generated with Claude Code

jonax1337 and others added 16 commits May 18, 2026 20:42
Foundation for the upcoming Steuer-Rücklage feature. Adds the user's
tax-related stammdaten to settings, without any computation logic yet.

Migration 0014 (user_version 15):
- legal_form: 'freelancer' | 'trade'
- trade_tax_rate (REAL, default 4.0 = 400%)
- church_tax_rate (REAL, 0 | 0.08 | 0.09)
- tax_filing_status: 'single' | 'married'
- est_prepayment_q1..q4_cent (INTEGER)

UI: new 'Steuerprofil'-Card in Settings with Rechtsform-Select,
Hebesatz-Input (only when 'trade'), Kirchensteuer-Select, Familienstand-
Select, 4× quarterly Vorauszahlung-Inputs. Klar gelabelt als Vorhersage,
keine Steuerberatung.

Schema-Spiegel-Trias synchron bumped: CURRENT_SCHEMA 14→15 in backup.rs,
CURRENT_DB_SCHEMA_VERSION 14→15 in Settings.svelte.
Pure functions for tax estimation, fully unit-tested.

src/lib/tax/income.ts:
- estimateIncomeTax(profitCent, status, churchRate) → { est, soli, kist, total }
- § 32a EStG VZ 2024 (5-Zonen-Formel, Splittingtarif für married)
- Soli mit Milderungszone (§ 4 Abs. 2 SolzG)
- KiSt 0/8/9 % auf ESt
- Constants in TARIF_2024 — easy to swap when BMF announces new year

src/lib/tax/trade.ts:
- estimateTradeTax(profitCent, hebesatz, isFreelancer) → { tradeTax, estCredit, messbetrag }
- Freibetrag 24.500 €, Messzahl 3,5 %
- § 35 EStG Anrechnung gedeckelt auf 3,8 × Messbetrag
- Freelancer-Path: sofort 0

Tests verify against amtlichen BMF-Tarifrechner-Outputs für 6 Einkommensstufen
(unter Grundfreibetrag bis Reichensteuer-Zone), Soli-Freigrenze + Milderungs-
zone + voller Satz, KiSt 8/9, Splittingtarif < Einzeltarif, GewSt-Freibetrag,
ESt-Anrechnung bei niedrigem vs. hohem Hebesatz, Verlustfall.

23 neue Tests (15 ESt + 8 GewSt), insgesamt jetzt 52.
Hybrid tax source — BMF-Lohnsteuer-Rechner authoritative + always-current
when online, lokale § 32a Formel als Fallback offline.

src-tauri/src/bmf.rs:
- Tauri-Command fetch_bmf_income_tax(zvECent, status, year)
- ureq GET an bmf-steuerrechner.de/interface/<JAHR>Version1.xhtml
- 8s timeout, XML-Response via Substring-Match parsen
- LZZ=1 (Jahr), STKL=1/3 (single/married Splittingtarif), 30+ Pflicht-
  parameter mit Defaults — reicht LSTLZZ + SOLZLZZ aus.

src/lib/tax/bmf.ts:
- fetchBmfIncomeTax wrapper mit 24h-localStorage-Cache pro (year, status,
  zvE). Bei Offline / API-Fehler → null, Aufrufer fällt zurück.
- clearBmfCache() für Steuerjahres-Wechsel.

src/lib/tax/income.ts:
- estimateIncomeTaxLocal (renamed from estimateIncomeTax) bleibt die
  synchrone, deterministische § 32a-Formel — Tests + Offline-Fallback.
- estimateIncomeTax async: tries BMF first, falls back to local. Returns
  IncomeTaxWithSource mit 'bmf' | 'local'-Badge + tatsächlich verwendetem
  Jahr. UI kann das anzeigen.
- KiSt bleibt lokal (rate × ESt), egal welche Quelle die ESt liefert.

23 Tests bleiben grün (sync local-Pfad). BMF-Pfad ist UI-getriggert und
nicht unit-getestet — würde echten Netz-Request brauchen.
Phase 3 — UI auf Tarif-Library + BMF-Integration.

src/lib/dashboard/tax.ts:
- computeTaxRücklage() aggregiert Gewinn YTD, lineare Jahres-Hochrechnung,
  USt-Schuld YTD (oder 0 für Kleinunternehmer), ESt+Soli+KiSt (via async
  estimateIncomeTax → BMF/Lokal), GewSt mit § 35-Anrechnung, abzgl.
  Vorauszahlungen aus Steuerprofil.
- USt-Schuld approximiert als (invoice_vat - storno_vat - expense_vat ohne
  RC) — exakt genug für Liquiditäts-Vorhersage, echte UStVA bleibt in
  /reports/ustva.
- Optional remainingProfitOverrideCent für Was-wenn-Slider.

src/routes/TaxReport.svelte (new, /reports/taxes):
- Headline-Card mit empfohlener Rücklage in groß
- Datenquellen-Badge (BMF live / lokale Schätzung)
- Hochrechnungs-Card mit Was-wenn-Input
- Aufschlüsselungs-Tabelle (ESt + Soli + KiSt, GewSt mit Anrechnung,
  USt-Schuld YTD, Summe, Vorauszahlungs-Abzug, Empfohlene Rücklage)
- Disclaimer prominent: keine Steuerberatung

Dashboard.svelte:
- Neue Card unter den KPI-Cards, vor Aging/Wiedervorlage-Row
- Kompakt: Headline-Summe + ein-Zeilen-Aufschlüsselung + Hover-Link
- Tax-Compute außerhalb des Haupt-Promise.all (kann BMF-Roundtrip
  haben), Dashboard rendert vorab ohne zu warten

Route /reports/taxes in App.svelte registriert.
Optionaler Modus für User, die statt detaillierter Tarif-Rechnung lieber
einen Daumen-Wert nutzen — oder beides nebeneinander zur Kalibrierung.

Migration 0015 (user_version 16):
- use_pauschal_tax_reserve (BOOL, off)
- pauschal_tax_percent (REAL, 30.0)

Settings: Toggle in Steuerprofil-Card, bei aktiv ein Prozent-Input
(0-50, Step 1). Bezugsgröße ist Brutto-Umsatz YTD.

tax.ts erweitert:
- YearAggRow + SQL liefern revenueGross (total, Stornos negativ)
- Result hat pauschalReserveCent, pauschalDeltaCent, flags.usePauschal +
  pauschalPercent

UI:
- Dashboard-Card: zweite Zeile 'pauschal X%: Y €' wenn aktiv
- TaxReport: neue 'Detail vs. Pauschal'-Card mit Differenz + lesbarem
  Hinweis ('Detail höher — Pauschal unterschätzt' etc.)

Schema-Spiegel-Trias synchron bumped.
Two corrections to the v0.11 polish round:

1. Vorauszahlungen jahresgebunden. Bisher lagen die 4 Quartals-Werte als
   flache Spalten auf dem Settings-Singleton — implizit 'die 4 für irgendein
   Jahr', beim Jahreswechsel ginge die Historie verloren und gleichzeitiges
   Tracking von 2026er Q3 + 2027er Q1 (Anpassungsbescheid) wäre unmöglich.

   Migration 0016 (user_version 17) legt Tabelle tax_prepayments(year,
   quarter, amount_cent) an, UNIQUE(year, quarter). Bestehende 4 Settings-
   Spalten werden in das aktuelle Kalenderjahr migriert und bleiben als
   Deadweight in settings (in v0.12 droppen).

   Neues Query-Modul src/lib/db/tax-prepayments.ts:
   - listPrepayments(year) → immer 4 Rows (fehlende = 0)
   - upsertPrepayment(year, quarter, cents) via ON CONFLICT
   - sumPrepaymentsForYear(year)
   - listPrepaymentYears() für UI-History

   tax.ts ruft jetzt sumPrepaymentsForYear(currentYear) statt
   settings.estPrepayment...

   Settings-UI: Jahres-Switcher (ChevronLeft/Right) über den 4 Quartals-
   Inputs, save-on-blur via upsertPrepayment. User kann beliebige Jahre
   pflegen — Dashboard rechnet immer mit dem aktuellen Kalenderjahr.

2. Bits-UI Slider-Wrapper in src/lib/ui/Slider.svelte (shadcn-Style:
   Track + Range + Thumb mit Hover-Ring). Pauschal-Prozent in Settings
   nutzt den Slider mit 0/25/50-Markern + großer Wert-Anzeige statt
   Number-Input.

Schema-Spiegel-Trias synchron auf 17.
Root cause: I had hung 'h-1.5' on data-[orientation=horizontal] of the
plain <span> wrapping the Range. Bits-UI only sets data-orientation on
the primitive components (Root/Range/Thumb), not on arbitrary children
— so the conditional class never matched, the track had height 0, only
the thumb was visible.

Stripped to horizontal-only with explicit h-1.5 w-full on the track,
border-2 on the thumb for a slightly heftier look, plus cursor-grab/
active:grabbing for affordance.
… coverage

User reported the Tax-Card always showed 'lokale Schätzung' instead of
BMF live. Diagnostic via curl revealed the actual problem:

  GET .../interface/2024Version1.xhtml?code=ext2024 ...
  HTTP/1.1 302 Found
  Location: /error/fehler.xhtml?allgemeinerFehler=
            extern.interface.jahr.not.found

  GET .../interface/2026Version1.xhtml?code=extern ...
  HTTP/1.1 200 OK
  <lohnsteuer>
    <information>Der angegebene Zugriffscode existiert nicht!</information>
  </lohnsteuer>

The BMF interface requires a partner-registered Zugriffscode that
external apps don't have without applying via BMF Formular. My
'ext2024'/'extern'/'test' guesses all rejected. No path to BMF live
without registration.

Honest fix:
- Removed src-tauri/src/bmf.rs, src/lib/tax/bmf.ts, the Tauri command
  and the 24h localStorage cache. They were dead weight pretending to
  work.
- Multi-year Tarif table in src/lib/tax/income.ts:
  * TARIF_2024 (Inflationsausgleichsgesetz)
  * TARIF_2025 (Steuerfortentwicklungsgesetz Dez 2024)
  * TARIF_2026 (Steuerfortentwicklungsgesetz Dez 2024, Grundfreibetrag
    12.348 EUR + linear angepasste Eckwerte und § 32a-Coefficients)
- estimateIncomeTax wieder sync, year-Parameter wählt den TARIF,
  Zukunfts-Jahre nutzen den jüngsten hinterlegten.
- IncomeTaxResult.tarifYear echoed für UI.
- Aggregator + Dashboard + TaxReport vereinfacht (kein async, kein
  BMF/Lokal-Badge mehr, statt dessen 'Tarif YYYY').

Tests (38 income + 8 trade = 46 tax tests, 75 total):
- Pro Jahr: Grundfreibetrag-Boundary, Zone 2, Zone 3, Zone 4 Spitzen-
  satz, Zone 5 Reichensteuer, Splittingtarif, Soli unter Freigrenze,
  Soli in Milderungszone, KiSt 8/9
- Cross-Year-Properties: Steuerlast sinkt 2024→2025→2026 wegen
  Grundfreibetrag-Anhebung, Splittingtarif < Single jedes Jahr, total
  = est + soli + kist
- getTarifYear fallback für vergangene und Zukunfts-Jahre
- Danger Zone in Settings: 3 irreversible Aktionen (alle Geschäftsdaten / einzelne Tabellen / Counter-Reset), LÖSCHEN-Confirm, Auto-Backup vor jedem Wipe nach ~/Dokumente/Zettel/Backups/
- YTD- vs. Projektions-Modus in /reports/taxes und Dashboard-Widget — klickbare Cards für „Bisher ausgelöst" vs. „Aufs Jahr hochgerechnet"
- Fix Storno-Bug: eurTotalCent wurde positiv gespeichert bei Stornos, Dashboard-Aggregate haben Storno deshalb doppelt addiert statt rausgenettet. Migration 0018 repariert Bestand
- Fix Storno-Aggregation in tax.ts: separate Storno-Spalte zog doppelt ab
- Fix Dashboard-Saldo: Expense-Filter auf status IN ('open','paid'), Drafts zählen nicht mehr
- YoY ggü. Vorjahr entfernt (Display + dead code)
- Nebenberuf-Bug: marginale ESt-Berechnung ESt(other+selbst)−ESt(other) bei gesetztem other_income_annual_cent. Migration 0017
- „Was wenn"-Override aus TaxReport entfernt
- Schema-Trias auf 19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- computeTaxRücklage akzeptiert optionalen `year`-Parameter. Aggregation cappt YTD bei min(now, yearEnd), Vergangenheitsjahre laufen aufs volle Jahr (daysElapsed=365 → Projektion=YTD).
- Dashboard-Widget übernimmt automatisch das Jahr der gewählten Periode (Container-Jahr bei Quartal/Monat/Custom). Switch im Period-Selector aktualisiert die Steuer-Karte mit.
- TaxReport.svelte: Jahres-Dropdown (2024–2027) im Header, Default aus Dashboard-Periode. Calculator-Icon raus, sauberer Header mit Selector rechts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rust-Command `auto_backup_target` resolved <Documents>/Zettel/Backups/<filename>
zuverlässig serverseitig — vermeidet documentDir()/join() im Frontend, das auf
Windows je nach OneDrive-Setup zu „file not found" (OS error 2) führen kann.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toast zeigt jetzt die fehlgeschlagene Phase (backup / wipeAll / wipeTables /
resetCounters), Console.error mit dem rohen Fehler-Objekt für Stacktrace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jeder der 4 Schritte (auto_backup_target → snapshot_db_path → VACUUM INTO →
bundle_backup) wirft jetzt mit eigenem Präfix, damit der OS-error-2-Bug
eindeutig zuordenbar wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t da

Bug-Repro: User mit OneDrive-Documents (oder ohne Documents-Folder) bekam
OS error 2 von `app.path().document_dir()` bzw. `create_dir_all`. Auto-Backup
versucht jetzt zuerst <Documents>/Zettel/Backups/, fällt bei jedem Fehler
auf <AppData>/<identifier>/Backups/ zurück — der App-Data-Pfad existiert
garantiert, weil die App schon daraus läuft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jonax1337 jonax1337 merged commit 63b0bbf into main May 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant