feat(v0.11): Steuer-Rücklage, Danger Zone, Storno-Fixes#68
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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/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 #66tax_prepayments-Tabelle (statt fixer Q1–Q4-Spalten an settings).ESt(other+selbst) − ESt(other). Behebt den Nebenberufler-Bug, bei dem die Selbst-Einnahmen sonst im falschen Grenzsteuersatz lagen.LÖSCHEN, Auto-Backup vor jedem Wipe (Documents/Zettel/Backups/ mit App-Data-Fallback).Bug-Fixes
createInvoice/updateInvoicesetzen jetzt das gleichesign-Vorzeichen für eurTotalCent wie für subtotal/total. Migration 0018 repariert Bestandsdaten.SUM(subtotal)mit natürlichem Vorzeichen umgestellt.status IN ('open','paid')gezogen.Migrationen
0014_v0.11_tax_profile.sql— Steuerprofil-Spalten an settings0015_v0.11_pauschal_mode.sql—pauschal_tax_percent+use_pauschal_tax_reserve0016_v0.11_tax_prepayments.sql— Multi-Year Tabelle, Migration der Bestands-Q1-Q4 ins neue Format0017_v0.11_other_income.sql—other_income_annual_cent0018_v0.11_fix_storno_eur_total.sql— Vorzeichen-Repair für bestehende StornosSchema-Trias (lib.rs / backup.rs / Settings.svelte) auf 19.
Closes
Test plan
LÖSCHENmuss exakt getippt sein, Button bleibt sonst disabled🤖 Generated with Claude Code