Entwicklung¶
Diese Seite richtet sich an alle, die an Haben mitarbeiten wollen: lokale Umgebung, Befehle, Tests, Migrationen, Konventionen im Code und typische Erweiterungen. Den Aufbau im Detail beschreibt Architektur.
Voraussetzungen¶
- Node.js 22 (
engines.nodeverlangt mindestens 22) - pnpm 10 (das Repository legt
pnpm@10.28.0überpackageManagerfest; mitcorepack enablebekommst du die passende Version) - PostgreSQL 16, lokal oder als Container
- Optional: ein C-Compiler (
cc) für die ERiC-Tests mit nachgebauter Bibliothek, Java 21 für den KoSIT-Validator
Lokale Umgebung¶
git clone https://github.com/firsttris/haben.git
cd haben
pnpm install
cp apps/web/.env.example apps/web/.env
In apps/web/.env trägst du mindestens DATABASE_URL, BETTER_AUTH_SECRET und HABEN_ENCRYPTION_KEY ein (beide mit openssl rand -base64 32). BETTER_AUTH_URL bleibt http://localhost:3000. Alle Variablen erklärt Betrieb und Installation.
[!NOTE] Leere Zeilen wie
ELSTER_HERSTELLER_ID=gelten als nicht gesetzt. Ohne ERiC (ERIC_HOMEoder unter Einstellungen geladen nachERIC_DIR) arbeitet Haben mit dem simulierten ELSTER-Client, ohneANTHROPIC_API_KEYohne KI-Auslesung. Für die Entwicklung reicht beides.
Datenbank anlegen und Migrationen anwenden. Das Migrationsskript liest DATABASE_URL aus der Umgebung, nicht aus der .env-Datei:
createdb haben
export DATABASE_URL=postgres://haben:haben@localhost:5432/haben
pnpm db:migrate
pnpm dev # http://localhost:3000
Belegdateien landen ohne weitere Angabe in apps/web/data/belege (DOCUMENTS_DIR, von Git ignoriert).
Befehle¶
| Befehl | Was er tut |
|---|---|
pnpm dev |
Dev-Server der Web-App auf Port 3000 (Vite) |
pnpm build |
Baut alle Pakete, die ein build-Skript haben (praktisch apps/web) |
pnpm typecheck |
tsc -p . in jedem Paket |
pnpm lint |
ESLint über das ganze Repository |
pnpm test |
Alle Tests mit Vitest |
pnpm db:generate |
Migration aus Schemaänderungen erzeugen (drizzle-kit) |
pnpm db:migrate |
Ausstehende Migrationen anwenden |
pnpm --filter @haben/web start |
Gebaute App starten (.output/server/index.mjs) |
pnpm --filter @haben/einvoice kosit [ausgabeverzeichnis] |
Beispielrechnungen erzeugen und mit dem KoSIT-Validator prüfen |
pip install -r requirements-docs.txt && mkdocs serve |
Diese Dokumentation als Website lokal auf Port 8000 (MkDocs Material); mkdocs build --strict wie in der CI |
pnpm docs:screenshots |
Nimmt die Bilder in README und Doku neu auf (docs/screenshot-*.png, siehe Bilder für README und Doku) und danach das Vorschaubild |
pnpm docs:social-preview |
Rendert scripts/social-preview/social-preview.html nach docs/social-preview.png (1280 × 640, Bild für Settings → Social preview) |
Tests¶
Die Tests laufen mit Vitest. vitest.config.ts sammelt alle *.test.ts unter apps/*/src und packages/*/src ein und führt die Dateien nacheinander aus, weil sich die Integrationstests eine Datenbank teilen.
pnpm test
TEST_DATABASE_URL=postgres://haben:haben@localhost:5432/haben_test pnpm test
pnpm vitest run packages/core # nur ein Bereich
Die Integrationstests in apps/web/src/server brauchen TEST_DATABASE_URL. Ohne die Variable werden sie übersprungen. Mit ihr löscht apps/web/src/server/test-db.ts die Schemas public und drizzle und wendet alle Migrationen neu an.
[!IMPORTANT]
TEST_DATABASE_URLniemals auf eine Datenbank mit echten Daten zeigen lassen. Die Tests leeren sie vollständig.
Die ERiC-Tests mit nachgebauter Bibliothek (packages/elster/test-fixtures) kompilieren eine kleine C-Bibliothek mit cc. Gibt es keinen Compiler, werden sie übersprungen.
Tests mit echtem ERiC¶
ERiC darf nicht im Repository oder in der CI liegen. Ist es lokal da (ERIC_HOME oder per install-cli.ts nach ERIC_DIR geladen), laufen zwei Ebenen von Tests zusätzlich gegen das echte ERiC. Gesendet wird dabei nichts, ERiC prüft nur lokal Schema und Plausibilität (ERIC_VALIDIERE):
packages/elster/src/eric-echt.test.tsbaut jede Nachricht in vielen Varianten: Voranmeldung 2025/2026 mit allen Kennzahlen, Umsatzsteuererklärung, Anlage EÜR mit AVEÜR und Einkommensteuer für 2023 bis 2025 mit allen Feldern, Sonstige Nachricht, Bankverbindung, Belegabruf, Abrufberechtigung und Postfach. Ohne ERiC wird die Datei übersprungen.- Die Integrationstests in
apps/web/src/servernutzenEricGeprueftClient(test-eric.ts). Mit ERiC lässt er jedes XML, das aus den Szenarien der Datenbank entsteht, zusätzlich von ERiC prüfen.
node --experimental-strip-types packages/elster/src/install-cli.ts --lizenz-akzeptiert # einmalig, lädt nach ERIC_DIR (Standard data/eric)
ERIC_DIR=data/eric pnpm vitest run packages/elster
ERIC_DIR=data/eric TEST_DATABASE_URL=postgres://haben:haben@localhost:5432/haben_test pnpm test
Seit ERiC 39.4 ist die frühere Test-Hersteller-ID 74931 gesperrt, auch für die lokale Prüfung. Die Tests nehmen ELSTER_HERSTELLER_ID, sonst eine Platzhalter-ID; für die reine Prüfung reicht das. Die Schemas der Formulare stecken komprimiert in den Plugins unter lib/plugins; meldet ERiC ein unerwartetes Feld, lohnt der Blick dort hinein.
Für Belegabruf, Berechtigung und Postfach gibt es zusätzlich ein Kommando, das auch unter Einstellungen › ELSTER › ERiC als Knopf steckt (Exit-Code 1 bei Fehlern):
node --experimental-strip-types packages/elster/src/check-formats-cli.ts # ERiC aus ERIC_HOME oder ERIC_DIR
| Bereich | Was getestet wird |
|---|---|
packages/core |
Beträge und Rundung, Rechnungssummen netto und brutto und Nummernformat, Zeiträume und Fälligkeit der Voranmeldung, Feiertage je Bundesland, Steuerfälle von Rechnungen, Steuernummer-Umrechnung, Kennzahlen der UStVA, Buchungssätze für SKR03/SKR04 und Ist/Soll, Zuordnungsvorschläge im Bankabgleich, EÜR, Umsatzsteuer des Lexoffice-Altbestands |
packages/elster |
UStVA-XML (Kopf, Kennzahlen, Testmerker, Kz 09/10), XML der Umsatzsteuererklärung, der Anlage EÜR mit AVEÜR, der Einkommensteuer und der Sonstigen Nachricht, Transfer-Ticket, simulierter Client, Kindprozess (Zertifikat mit 0600, Timeout, Absturz), Mock-ERiC über koffi, ERiC-Download (Entpacken nur Linux, defektes Archiv, Umschalten erst bei vollständiger Version, Pfade im Archiv); mit echtem ERiC alle Nachrichten in vielen Varianten (siehe oben) |
packages/einvoice |
Rechnungs-PDF (PDF/A-3b), ZUGFeRD und XRechnung (CII, UBL) inklusive Storno, Leitweg-ID und Bruttopreisen, Pflichtangaben je Format, Einlesen fremder E-Rechnungen und eingebetteter XML |
packages/import |
DKB- (neu und alt), N26- und CAMT.053-Parser, Deduplizierung, Saldenprüfung, DATEV-Buchungsstapel, Lexoffice-API-Client (Paging, 429, Fehler) und Abbildung der Lexoffice-Daten |
apps/web ohne DB |
Umwandlung der KI-Auslesung in Felder und Beträge |
apps/web mit DB |
Rechnungen festschreiben und sperren, Belege hochladen und buchen, Bankimport und Zuordnung, Voranmeldung (Entwurf, Test, Echt, Berichtigung, Audit-Log) und Berechnung aus Buchungen, Auswertungen, Jahresexport, Lexoffice-Übernahme und offene Posten |
KoSIT-Validator¶
Der CI-Job kosit erzeugt mit packages/einvoice/scripts/kosit-check.ts Beispielrechnungen in allen Formaten (19 %, gemischte Sätze, Storno, Korrektur, Leitweg-ID, nur Steuernummer, Nullsatz, Reverse Charge, Drittland, steuerfrei, Kleinunternehmer, Bruttopreise) und prüft sie mit dem KoSIT-Validator und der XRechnung-Konfiguration. Lokal brauchst du Java 21 sowie den Validator und die Konfiguration von GitHub (itplr-kosit/validator, itplr-kosit/validator-configuration-xrechnung; welche Versionen CI nutzt, steht in .github/workflows/ci.yml):
KOSIT_JAR=/pfad/validationtool-1.5.0-standalone.jar \
KOSIT_CONFIG=/pfad/scenarios.xml \
pnpm --filter @haben/einvoice kosit /tmp/haben-kosit
Ohne die Variablen sucht das Skript unter /var/tmp/kosit/.
Ende-zu-Ende-Tests mit Playwright¶
apps/web/e2e/app.spec.ts spielt den Betrieb in einer leeren Datenbank durch:
1. Konto und Firmendaten einrichten.
2. Kunden anlegen.
3. Rechnungen schreiben und festschreiben.
4. Einen Beleg hochladen und buchen. Das Belegbild entsteht im Test, mit erfundenen Daten.
5. Einen Kontoauszug importieren und die Umsätze zuordnen.
6. Prüfen, dass Kennzahlen und Salden unter Konten dazu passen, und den Filter im Journal testen.
Zum Schluss lädt der Test jede Seite am Desktop (1440 px) und am Handy (390 px) und prüft dabei: - Die Seite antwortet ohne Fehler und hat eine Überschrift. - Im Browser tritt kein Fehler auf, auch kein Hydration-Fehler. - Am Handy ist die Seite nicht breiter als der Bildschirm. - Das Handy-Menü lässt sich öffnen und bedienen.
Von jeder Seite legt der Test einen Screenshot unter apps/web/test-results/screenshots/{desktop,mobil}/ ab.
createdb haben_e2e # einmalig; der Name muss auf „e2e“ enden, die Datenbank wird bei jedem Lauf geleert
E2E_DATABASE_URL=postgres://haben@localhost:5432/haben_e2e pnpm e2e
Playwright startet selbst einen Server auf Port 3100 (E2E_PORT). Vorher leert es die Datenbank und spielt die Migrationen ein. Lokal ist das der Entwicklungsserver, in der CI der gebaute Server (E2E_SERVER_COMMAND="node .output/server/index.mjs" nach pnpm build). Chromium kommt einmalig mit pnpm --filter @haben/web exec playwright install chromium. Wer schon ein passendes Chromium hat, setzt stattdessen PLAYWRIGHT_CHROMIUM_EXECUTABLE.
In der CI läuft der Job e2e bei jedem Pull Request. Screenshots, Playwright-Bericht und bei Fehlern Traces hängen als Artefakt e2e-screenshots am Lauf. So sieht man zu jeder Änderung, wie alle Seiten aussehen.
Bilder für README und Doku¶
Die Bilder in README und Doku (docs/screenshot-*.png) entstehen aus denselben Daten: nach app.spec.ts
läuft e2e/zz-doku-screenshots.spec.ts, meldet sich an und fotografiert Übersicht, Rechnungseditor,
Bankabgleich, Beleg, Voranmeldung, Auswertungen, Archiv und die Übersicht am Handy. Ohne
DOKU_SCREENSHOTS=1 überspringt es sich selbst, die normalen Tests bleiben unverändert.
Nach einer Änderung am Aussehen den Workflow Update screenshots (Actions → Run workflow) auf dem Branch starten: er nimmt die Bilder im offiziellen Playwright-Image mit einer eigenen Datenbank auf und committet, was sich geändert hat.
Migrationen¶
Das Schema steht in apps/web/src/server/db/schema.ts, die Migrationen in apps/web/drizzle/.
- Schema anpassen.
pnpm db:generateerzeugt die SQL-Migration und den Snapshot indrizzle/meta/.pnpm db:migratewendet sie lokal an. Im Container passiert das beim Start automatisch.
Trigger und Funktionen (Festschreibung, Versionierung, Audit-Log) kann drizzle-kit nicht aus dem Schema ableiten. Sie stehen in eigenen SQL-Migrationen, z. B. 0001_festschreibung.sql, 0003_rechnungen_trigger.sql oder 0010_lexoffice_trigger.sql. Eine leere Migration dafür legst du so an und schreibst das SQL von Hand hinein:
Bestehende Migrationen änderst du nicht; sie sind auf laufenden Installationen schon angewendet.
Konventionen¶
- Geld in ganzen Cent (
Cents), nie als Gleitkommazahl. Parser rechnen Dezimaltext direkt in Cent um. - Steuersätze in Basispunkten (
BasisPoints): 1900 = 19 %, 700 = 7 %. - Mengen in Tausendsteln: 152 Stunden = 152000, 0,5 Tage = 500.
- Imports mit
.ts-Endung (allowImportingTsExtensions). Der ERiC-Worker und das Migrationsskript laufen per Type Stripping direkt aus den Quellen. - Oberfläche auf Deutsch, auch Fehlermeldungen, die beim Nutzer ankommen.
- Unveränderlichkeit (GoBD) in der Datenbank: Festgeschriebene Rechnungen, Buchungen, gesendete Voranmeldungen, Bankumsätze und übernommene Lexoffice-Daten sperren Postgres-Trigger gegen
UPDATEundDELETE. Korrekturen sind neue Datensätze (Storno, Gegenbuchung, berichtigte Anmeldung). Beim Festschreiben einer Buchung prüft ein Trigger Soll = Haben. - Audit-Log: Änderungen laufen über
withActor(apps/web/src/server/db/actor.ts), damit der Trigger sie dem Nutzer zuschreibt. - Server-Funktionen bleiben dünn: In
apps/web/src/server/functions/hängt jede Funktion mit DatenauthMiddlewarean, validiert die Eingabe mit Zod und ruft die Logik im passenden Modul unterapps/web/src/server/modules/auf. Fachlogik ohne Datenbank gehört nachpackages/core. - Modulgrenzen: Aus einem anderen Modul nur über dessen
index.tsimportieren; die Lint-Regelhaben/modulgrenzenbricht die CI sonst ab. Fehlt dort ein Name, ergänze ihn in derindex.ts. Neuer Code gehört in das Modul, dessen Tabellen er schreibt (Module im Server). - ERiC nie im App-Prozess: Jeder Aufruf geht über einen kurzlebigen Kindprozess (
packages/elster/src/worker.ts).
Projektstruktur¶
| Pfad | Inhalt |
|---|---|
apps/web/src/routes |
Seiten (TanStack Router) und API-Routen unter api/ |
apps/web/src/server |
modules/ mit der Serverlogik je Fachbereich (öffentlich über index.ts), db/ mit Schema und Migrationsskript, functions/ mit den Server-Funktionen, daneben gemeinsame Infrastruktur |
eslint/ |
Eigene Lint-Regel für die Modulgrenzen |
apps/web/src/components, lib |
React-Komponenten, Auth-Client, Formatierung |
apps/web/drizzle |
SQL-Migrationen |
packages/core |
Beträge, Zeiträume, Steuernummer, UStVA, Buchungssätze, EÜR, Zuordnungsvorschläge |
packages/elster |
ERiC-Anbindung, XML für Voranmeldung und Jahreserklärungen, Worker |
packages/import |
Kontoauszugs-Parser, Deduplizierung, DATEV, Lexoffice-Client |
packages/einvoice |
Rechnungs-PDF mit Typst, E-Rechnung erzeugen und lesen |
deploy |
Quadlets, Caddyfile, Backup- und Startskript |
docs, mkdocs.yml |
Diese Dokumentation; auf main baut .github/workflows/docs.yml sie nach https://firsttris.github.io/haben/ |
Neues Bankformat¶
Die Parser liegen in packages/import/src/.
- Eigene Datei anlegen, z. B.
meinebank.ts, mit einer Erkennungsfunktion für die Kopfzeile (isMeinebankHeader) und einem Parser, der einParsedStatementliefert.dkb.tsundn26.tssind gute Vorlagen; Hilfen für Beträge, Datum und IBAN stehen intext.ts,table.tsundcommon.ts. - Das Format in
StatementFormatintypes.tsergänzen. - In
parse.tsdie Erkennung indetect()und den Fall inparseStatement()eintragen. Erkannt wird am Inhalt, nicht am Dateinamen. - Eine anonymisierte Beispieldatei unter
packages/import/test-fixtures/ablegen und Tests inparse.test.tsergänzen, auch für die Deduplizierung.
Das Format wird als Text in bank_imports.format gespeichert; eine Migration ist dafür nicht nötig. Liefert die Datei keine eigene IBAN, muss das Konto vor dem Import angelegt werden (wie bei N26).
Neue Ausgabenkategorie¶
Die Kategorien stehen in EXPENSE_CATEGORIES in packages/core/src/posting.ts, je mit Bezeichnung und Aufwandskonto für SKR03 und SKR04. Ein neuer Eintrag erscheint automatisch in der Auswahl am Beleg, in der Anweisung für die KI-Auslesung, im Journal und in der EÜR. Die Kategorie steht als Text am Beleg, eine Migration ist nicht nötig. Für übernommene Lexoffice-Belege kannst du in apps/web/src/server/modules/archiv/legacy-open.ts (CATEGORY_HINTS) ein Suchmuster ergänzen. Die Kontonummern stimmst du mit der Steuerberatung ab.
Veröffentlichen¶
Images baut der Workflow .github/workflows/release.yml: die CI liegt hier, Image, Docker-Hub-Beschreibung und GitHub-Release kommen aus dem gemeinsamen docker-release.yml in firsttris/workflows. Ein Tag vX.Y.Z veröffentlicht eine Version:
- Der Tag muss zu
versionin derpackage.jsonim Wurzelverzeichnis passen. - Die komplette CI läuft (Lint, Typecheck, Tests, Build, KoSIT, Container-Test), außer die CI auf
mainhat denselben Code schon geprüft: ein Commit von Bump version ändert nur die Version, dann zählt der grüne Lauf des Commits davor. - Das Image wird für
linux/amd64undlinux/arm64gebaut und als:x.y.z,:x.yund:latestnach Docker Hub (tristanteu/haben) und GHCR (ghcr.io/firsttris/haben) geschoben. Die README landet mit absoluten Links als Beschreibung auf Docker Hub. - Erst danach entsteht das GitHub-Release mit erzeugten Notizen.
Den Tag legt einer von zwei Wegen an, nach demselben Schema wie in den anderen Projekten:
- ohne Checkout: Actions → Bump version → patch, minor oder major (
bump.yml). Erhöht die Version in derpackage.json, committet sie alsRelease vX.Y.Zaufmain, taggt und startetrelease.ymlauf dem Tag. - lokal:
pnpm release:patch # oder :minor, :major: erhöht die Version, legt Commit und Tag an und pusht beides
Von Hand gestartet (Actions → Release → Run workflow) laufen auf main dieselben Prüfungen, danach wird nur :edge veröffentlicht, ohne Release.
Der Workflow braucht das Repository-Secret DOCKER_PAT, ein Docker-Hub-Zugriffstoken mit Schreibrecht für tristanteu/haben. Für GHCR reicht das eingebaute GITHUB_TOKEN; das Paket ist nach dem ersten Push privat und muss einmal in den Paket-Einstellungen auf öffentlich gestellt werden.
Mitwirken¶
Vor einem Pull Request bitte dieselben Prüfungen wie in CI laufen lassen:
pnpm lint
pnpm typecheck
TEST_DATABASE_URL=postgres://haben:haben@localhost:5432/haben_test pnpm test
pnpm build
- Neue Fachlogik mit Tests, Änderungen an Buchungen oder Steuerberechnung mit Testfällen für SKR03 und SKR04 sowie Ist und Soll.
- Schemaänderungen mit erzeugter Migration; Trigger in einer eigenen SQL-Migration.
- Änderungen an der E-Rechnung prüft der KoSIT-Job in CI.
- Texte in der Oberfläche auf Deutsch.
Haben steht unter der AGPL-3.0.