Projekte, Kunden & Baustellen — Stammdaten, Quick-Create, OCR, Stundennachweis
Feinkonzept · §3.4 Projekte, Kunden & Baustellen — Stammdaten, Quick-Create, Visitenkarten-OCR, Stundennachweis
Abschnitt betitelt „Feinkonzept · §3.4 Projekte, Kunden & Baustellen — Stammdaten, Quick-Create, Visitenkarten-OCR, Stundennachweis“Einordnung. Dieses Feinkonzept setzt FUNKTIONSUMFANG §3.4 um. Es ist das Rückgrat der operativen Stammdatenwelt: ohne saubere Kunden-, Projekt- und Baustellen-Entitäten landet kein Stempel auf einem Kostenträger, kein Stundennachweis trägt eine Adresse, kein DATEV-Export hat eine Debitorennummer. Die Disziplin aus LESSONS-LEARNED §9 („keine 70 Routes zur Sicherheit“) gilt hier besonders: wir bauen die drei Entitäten als eine fachliche Einheit mit klaren Feld-Pflichtstufen — nicht drei halbherzige CRUD-Module.
1. Header & Metadaten
Abschnitt betitelt „1. Header & Metadaten“feature_id: kern/04-projekte-kunden-baustellentitle: Projekte, Kunden & Baustellen — Stammdaten, Quick-Create, OCR, Stundennachweisfunktionsumfang_ref: §3.4roadmap_horizont: MVPplattformen: mobile: vollständig # Quick-Create, Visitenkarten-OCR, Favoriten, Signatur web: mit-Bulk # Vollformular, Budgets, Stundensätze, Massen-PDF-Versand desktop: ab V1.5 bei Bedarfowner_rolle: Manager # Stammdaten-Pflege primär Manager/Admin, Anlage auch Mitarbeitermodul_gate_flag: module.kern.stammdatencompliance_flags: gobd: true # Stundennachweis + Stundensätze sind abrechnungsrelevant (§147 AO) arbzg: false # Keine Zeit-Erfassungslogik hier; nur Zuordnung vob: false # VOB-Nachtrag/LV liegen in eigenem Feinkonzept dsgvo: true # Kunden-Ansprechpartner = personenbezogene Daten bfsg: false # Stammdaten-Pflege ist Admin-Pfad (Web-exklusiv) betrvg: false # Keine Mitarbeiter-Tracking-Funktion stvg: false weitere: ["§14 UStG Rechnungs-Pflichtfelder", "§48b EStG Freistellungs-Nachweis (Bau)"]referenzkunde: status: TBD name: "zu klären mit Sales — Zielprofile: Malerbetrieb 15–30 MA, SHK-Betrieb 60–100 MA" quelle: Interview-Protokoll (musterbetrieb-maler Scoping-Gespräch 2026-03-28)estimate_eng_tage: 22 # MVP-Slice: Entitäten + Quick-Create + Visitenkarten-OCR + Stundennachweis-PDFabhängigkeiten: - kern/09-auth-self-service # Scope-Model + Tenant-Setting für RLS - kern/11-compliance-audit # Audit-Log für jede Stammdaten-Mutation - kern/03-zeiterfassung # Projekt-/Baustellen-Zuordnung wird beim Stempeln konsumiert2. Kontext & Problem
Abschnitt betitelt „2. Kontext & Problem“Marktrealität (DE-Handwerk). Der 18-Mann-Malerbetrieb in Bayern führt heute Kunden in Excel, Baustellen in einer Klarsichthülle und Stundennachweise auf einem DIN-A5-Block, den der Bauherr vor Ort abzeichnet. Der 80-MA-SHK-Betrieb in NRW hat eine ERP-Schattenlösung (Sander & Doll oder pds), die exportieren kann, aber niemand im Feld benutzt — der Monteur ruft die Stammdaten nicht ab, er kritzelt auf Papier. Beide Welten leiden an demselben Bruch: Stammdaten leben im Büro, der Auftrag entsteht am Tresen des Bauherren. Wer dort keine Kunde-anlegen-in-15-Sekunden-Funktion hat, verliert die Zuordnung und damit §14-UStG-Pflichtfelder, Debitorennummer, Ansprechpartner, Zahlungsziel.
Rechtlich flankiert wird das durch §14 UStG (Pflichtangaben auf Rechnungen — ohne Kundensteuernummer/USt-ID bei B2B droht Vorsteuer-Ausschluss) und §48b EStG (bei Bau-Subunternehmern braucht der Auftraggeber eine Freistellungsbescheinigung, sonst 15 % Bauabzugssteuer). Beides landet in den Kunden-Stammdaten — nicht in einem separaten „Tax-Modul“.
Schmerzpunkt der Alt-App. Die Alt-App hatte Kunden, Projekte und Baustellen als drei lose Tabellen ohne gemeinsames Anlage-Fenster und ohne Quick-Create im Feld (LESSONS-LEARNED §2 — Feature-Wildwuchs mit 97 Tabellen). Der Monteur hat im Keller keinen Kunden gefunden, weil keiner angelegt war, und hat den Stempel auf „Sonstiges/Intern“ gebucht — die Stunde war für die Abrechnung verloren. Zusätzlich lag die gesamte Tenant-Trennung in WHERE tenant_id = ?-Klauseln (LESSONS-LEARNED §3); eine vergessene Klausel hätte Kundendaten von Tenant A in der Such-Autocomplete von Tenant B sichtbar gemacht.
Erwarteter Outcome.
- Quick-Create in ≤ 15 Sekunden auf Mobile ohne Blocker für den Workflow („Stempel jetzt, Feld-Pflege später“).
- Visitenkarten-OCR: Neukunde via Foto → 5 Felder vorausgefüllt → Bestätigung → angelegt. Zielzeit 30 s inkl. Foto.
- Stundennachweis-PDF aus Zeit-Buchungen pro Woche/Monat, Touch-Signatur durch Bauherr vor Ort (Mobile), Bulk-Versand per E-Mail (Web).
- Keine stillen Datenlecks. Postgres-RLS + Multi-Tenant-Test-Invariante (DOD §2.2) verhindern, dass ein vergessener
WHERETenant-Daten bloßlegt.
3. Personas & Rollen
Abschnitt betitelt „3. Personas & Rollen“| Rolle | Aktion | Scope | Plattform |
|---|---|---|---|
| Mitarbeiter (Monteur) | Projekt/Baustelle wählen, Quick-Create Kunde/Baustelle, Stundennachweis signieren lassen | own (eigene Stempelung, keine fremden Kundenlisten) |
📱🌐 |
| Bauleiter | Baustellen-Details lesen, Fortschritt kommentieren, Bauherr-Signatur einholen | team (alle Baustellen seines Teams) |
📱🌐 |
| Manager | Projekte anlegen, Budgets pflegen, Stundennachweise bulk-generieren und versenden | team / all per Admin-Freigabe |
📱🌐 |
| Admin | Kunden-Vollformular (UStID, Debitorennr., §48b EStG-Freistellung), Stundensätze, Lohnarten-Mapping | all |
🌐 |
| Buchhaltung | Kunden-Bank-/UStID-Felder pflegen, DATEV-Debitorennr. abgleichen | all (Schreibschutz auf Personalfeldern durch HR) |
🌐 |
Persona-Skizzen.
- Hannes Krüger (Monteur, 38, SHK, Tenant
shk-gebruder-schmidt): arbeitet mit Handschuhen, Empfang oft Edge/kein LTE, will drei Bauplätze dieser Woche ohne Tippen auswählen. - Sabine Maier (Manager, 45, Malerbetrieb
musterbetrieb-maler): verwaltet 25 MA, pflegt Stammdaten im Büro, erstellt freitags Stundennachweise für 3–5 Baustellen. - Thomas Schmidt (Bauleiter SHK, 52): Baustellen-Tournee mit Tablet, holt Bauherr-Unterschriften ein, übergibt Stundennachweis-PDF direkt per E-Mail.
- Frau Müller (Buchhaltung, 56,
shk-gebruder-schmidt): kennt DATEV-Debitoren, will UStID-Prüfung per BZSt-Abgleich und einmal-im-Monat-Export ohne Klickstrecke.
4. User-Stories
Abschnitt betitelt „4. User-Stories“US-01 [MVP] Als Monteur möchte ich offline eine Baustelle aus meiner Favoritenliste wählen, um ohne LTE-Empfang im Keller stempeln zu können.
US-02 [MVP] Als Monteur möchte ich im Feld in ≤15 Sekunden einen neuen Kunden anlegen (nur Pflichtfelder), damit ich nicht auf „Intern" stempeln muss.
US-03 [MVP] Als Monteur möchte ich eine Visitenkarte abfotografieren und die Felder vorgefüllt bestätigen, um beim Bauherr-Termin keine Tipparbeit zu haben.
US-04 [MVP] Als Manager möchte ich einen Stundennachweis für einen Mitarbeiter wochenweise als PDF erzeugen und an den Bauherrn senden, damit die Abnahme dokumentiert ist (§14 UStG-konform).
US-05 [MVP] Als Manager möchte ich in der App einen Bauherr-Stundennachweis per Touch-Signatur direkt vor Ort einholen, um den Abrechnungs- streit im Nachgang zu vermeiden.
US-06 [MVP] Als Admin möchte ich Stundensätze pro Rolle/Mitarbeiter/Datum pflegen, damit Projekt-Budgets realistisch gerechnet werden (ein Satz 2025 ≠ ein Satz 2026).
US-07 [MVP] Als Buchhaltung möchte ich die UStID eines Neukunden per BZSt validieren lassen, damit Rechnungen nicht an ungültige IDs gehen.
US-08 [V1] Als Admin möchte ich Projekt-Budgets (Zeit + Geld) anlegen und Warnschwellen definieren, damit ich vor Ausuferung gewarnt werde.
US-09 [V1] Als Manager möchte ich Stundennachweise bulk für alle Mitarbeiter eines Projekts generieren und an eine Bauherr-E-Mail versenden.
US-10 [V1.5] Als Admin möchte ich §48b-EStG-Freistellungsbescheinigungen als Foto hinterlegen und Ablaufdatum+Erinnerung bekommen, damit ich nicht 15 % Bauabzugssteuer einbehalten muss.5. Funktionale Anforderungen
Abschnitt betitelt „5. Funktionale Anforderungen“5.1 Mobile App (📱) — iOS + Android
Abschnitt betitelt „5.1 Mobile App (📱) — iOS + Android“- F-M-01 (Stammdaten-Browser) — Listen-Screen mit drei Tabs „Kunden / Projekte / Baustellen“. Suchfeld oben (Fulltext auf Postgres
tsvector-Index, offline über Drift-FTS5). Favoriten-Stern pro Eintrag. Default-Sortierung: zuletzt-verwendet durch mich. - F-M-02 (Favoriten & eigene Beteiligungen) — Pro User eine Favoriten-Liste (
user_favorites-Tabelle). Eigene Beteiligungen werden implizit durch Stempelungen der letzten 14 Tage gezeichnet (kein Nutzer muss Favoriten aktiv pflegen, aber darf). - F-M-03 (Detail-Screen Kunde/Baustelle) — Adresse, Ansprechpartner (Name, Telefon-Link, E-Mail-Link), Stundenstand (aggregiert aus Zeit-Buchungen der letzten 30 Tage), „Stundennachweis erstellen“-CTA.
- F-M-04 (Quick-Create Kunde — Minimal-Form) — Pflicht: Firma ODER Name (eins von beiden), Straße, PLZ, Ort. Optional: Telefon, E-Mail. Keine UStID, keine Debitorennummer, keine Zahlungsbedingung — das sind Web-Admin-Felder, die die Buchhaltung nachpflegt. Der Datensatz wird als
draft_by_field = truemarkiert, sichtbar für Admin im Web-Dashboard „Felder pflegen“. - F-M-05 (Quick-Create Baustelle) — Pflicht: Bezeichnung, Adresse, zugeordneter Kunde. Optional: Geo-Pin (auto via GPS), Bauherr-Ansprechpartner. Erzeugt fehlt-Kunde-Dialog, falls kein Kunde verknüpft.
- F-M-06 (Visitenkarten-OCR) — Kamera öffnet, Live-Detection-Overlay (Rechteck um erkannte Karte), Foto-Erfassung. Google ML Kit / Apple Vision extrahieren Felder on-device (Firma, Name, Telefon, E-Mail, Adresse). Dedup-Prüfung: Fuzzy-Match auf Firma+PLZ+Straße-Präfix im lokalen Drift-Cache → bei Treffer Dialog „Kunde Y.X.Z existiert — zusammenführen oder neu?“. Kein Server-Roundtrip für OCR (on-device, LESSONS-LEARNED §4 — offline tauglich).
- F-M-07 (Stundennachweis erstellen + Bauherr-Signatur) — Wähle Mitarbeiter (oder „ich“), Baustelle, Zeitraum (Default: aktuelle Woche). App zeigt Zeit-Buchungen gruppiert nach Tag, aggregiert in Stunden+Tätigkeit. Touch-Signatur-Pad (
signature-Package,.wz-signature). PDF wird client-seitig viaprinting-Package gerendert — oder serverseitig via Typst, wenn online. Optional E-Mail-Versand nach Signatur. - F-M-08 (Stundennachweis-Versand per E-Mail) — App öffnet Share-Sheet mit vorgefülltem Betreff „Stundennachweis KW 16 – Baustelle Lehrer Allee 7“, Body-Template, PDF als Anhang. Kopie landet im Backend-Audit-Log mit
stundennachweis.sent.email.
5.2 Web-App (🌐)
Abschnitt betitelt „5.2 Web-App (🌐)“- F-W-01 (Kunden-Vollformular) — Alle Pflichtfelder nach §14 UStG für B2B: Firmenname, vollständige Adresse, UStID (optional für B2C), Steuernummer (optional), Debitorennummer (für DATEV), Zahlungsziel (Default 14 Tage), Skonto-Regeln, Bankverbindung (IBAN —
pgcrypto-verschlüsselt at-rest gem. TECH-STACK §11), Kontakt-Liste (Ansprechpartner mit Funktion/Telefon/E-Mail), §48b-Freistellungs-Foto + Gültigkeit. - F-W-02 (UStID-Validierung) — Button „UStID prüfen“ → Backend-Endpoint
/v1/customers/validate-vatruft BZSt-Abgleichs-SOAP-Schnittstelle auf (qualifizierte Abfrage mit Firma+PLZ+Straße). Ergebnis als 24h-Cache persistiert mitvat_validated_at. Rate-Limit 10/min pro Tenant (BZSt-Fair-Use). - F-W-03 (Projekt-Verwaltung) — Projekt = Klammer über mehrere Baustellen (z. B. „Wohnanlage Müller-Schule“ mit 3 Bauabschnitten). Felder: Name, Beschreibung, Start/Ende, Verantwortliche Bauleitung, Status (Angebot / Angenommen / In Arbeit / Abgeschlossen / Archiviert).
- F-W-04 (Projekt-Budgets) — Budget in Stunden + Budget in Euro + Warnschwelle (% von Budget). Live-Forecast basierend auf bisherigem Verbrauch. Wenn Warnschwelle überschritten → Badge am Projekt + Benachrichtigung an Bauleitung. (V1-Scope — MVP hat nur das Feld, Forecast-Berechnung folgt in V1.)
- F-W-05 (Baustellen-Verwaltung) — Adresse, Geo-Koordinaten, SOKA-Bau-Nr. (für Bau-Kunden), Kolonnen-Zuordnung, DGUV-Plakette-Ablauf. Listen-Filter: aktive / abgeschlossene / archivierte.
- F-W-06 (Stundensätze pro Rolle/Mitarbeiter/Datum) — Tabelle
hourly_ratesmitvalid_from/valid_to(SCD Type 2 — kein Update, neuer Datensatz). Zuweisbar pro Mitarbeiter, pro Rolle, pro Kunde-Override. Anzeige zeigt „effektiver Satz heute“ mit Vererbungs-Breadcrumb (Override > Mitarbeiter-Satz > Rollen-Default). - F-W-07 (Stundennachweis Bulk-Generierung) — Filter (Zeitraum, Projekt, Mitarbeiter, Status), Mehrfachauswahl, Aktion „Generieren & Versenden“. PDF-Generierung via Typst-Server (TECH-STACK §5.1), Versand via SES. Status-Tracking: Entwurf → Wartet-auf-Bauherr → Signiert → Versendet → In-Rechnung.
- F-W-08 (Stundennachweis-PDF — Layout) — Kopf: Tenant-Logo, Tenant-Adresse, Bauherr-Adresse, Zeitraum, Projekt/Baustelle, Mitarbeiter. Tabelle: Tag, Start, Ende, Pause, Netto-Stunden, Tätigkeit. Fußbereich: Signatur-Platzhalter, Hash-Chain-Footprint („Dokument-Integrität: sha256:
· Audit-Ref: “). Generator: Typst-Template stundennachweis.typ.
5.3 Cross-Plattform (🔄)
Abschnitt betitelt „5.3 Cross-Plattform (🔄)“- F-X-01 (Referenz-Integrität) — Kunde löschen → prüfe offene Baustellen+Zeit-Buchungen. Kaskaden-Löschung ist nicht erlaubt; stattdessen Soft-Delete via
archived_at-Feld. GoBD verhindert harte Löschung von Abrechnungs-Bezügen. - F-X-02 (Dedup-Assistent) — Bei jeder Anlage prüfe Fuzzy-Match (Trigram-Index auf
customers.name) und zeige ggf. „Ähnlicher Kunde vorhanden: Maler Schmidt GmbH — zusammenführen?“. - F-X-03 (Tenant-Hopping-Schutz) — Alle Lookups laufen über
current_setting('app.tenant_id')-basierte RLS-Policies. API-Endpunkte antworten auf fremde IDs mit 404 (nicht 403 — Existenz nicht offenbaren; konsistent mit §12 Akzeptanzkriterium).
5.4 Admin-Konfiguration
Abschnitt betitelt „5.4 Admin-Konfiguration“- F-A-01 (Draft-Queue) — Web-Dashboard „Stammdaten-Nachpflege“: alle Quick-Create-Einträge mit
draft_by_field = true. Buchhaltung arbeitet diese ab und setzt Flag auffalse, sobald UStID + Debitorennr. + Zahlungsziel gepflegt sind. - F-A-02 (Import/Migration) — CSV-Import aus Excel, Clockodo, ZEP, Personio. Mapping-Oberfläche, Preview, Dedup-Check, Rollback-Transaktion. Teil des Onboarding-Wizards.
- F-A-03 (§48b EStG-Erinnerung) — Admin-Einstellung „Erinnere mich an ablaufende Freistellungen“: 90/30/7 Tage vor Ablauf → Push + E-Mail. Ohne gültige Bescheinigung blockiert die Rechnungsstellung automatisch den Subunternehmer (konsistent mit FUNKTIONSUMFANG §4.8).
5.5 Roadmap-Schichtung
Abschnitt betitelt „5.5 Roadmap-Schichtung“| Anforderung-ID | MVP | V1 | V1.5 | V2 |
|---|---|---|---|---|
| F-M-01 Listen-Browser | ✅ | |||
| F-M-02 Favoriten | ✅ | |||
| F-M-03 Detail-Screen | ✅ | |||
| F-M-04 Quick-Create Kunde | ✅ | |||
| F-M-05 Quick-Create Baustelle | ✅ | |||
| F-M-06 Visitenkarten-OCR | ✅ | |||
| F-M-07 Stundennachweis + Signatur | ✅ | |||
| F-M-08 Stundennachweis-Versand | ✅ | |||
| F-W-01 Kunden-Vollformular | ✅ | |||
| F-W-02 UStID-Validierung | ✅ | |||
| F-W-03 Projekt-Verwaltung | ✅ | |||
| F-W-04 Projekt-Budgets | ✅ | |||
| F-W-05 Baustellen-Verwaltung | ✅ | |||
| F-W-06 Stundensätze (SCD2) | ✅ | |||
| F-W-07 Bulk-Generierung | ✅ | |||
| F-W-08 PDF-Layout (Typst) | ✅ | |||
| F-A-01 Draft-Queue | ✅ | |||
| F-A-02 Import-Wizard | ✅ | |||
| F-A-03 §48b-Erinnerung | ✅ |
6. Mockups & Flows
Abschnitt betitelt „6. Mockups & Flows“HTML-Hero-Mockup: 04-projekte-kunden-baustellen.html — Mobile (Hannes Krüger, Baustellen-Wechsel mit Visitenkarten-OCR) + Web (Sabine Maier, Stundennachweis-Bulk-Generierung).
6.1 Mobile — Baustellen-Liste mit Quick-Create
Abschnitt betitelt „6.1 Mobile — Baustellen-Liste mit Quick-Create“┌────────────────────────────────┐│ ← Baustellen [+] │├────────────────────────────────┤│ 🛡 GoBD · DSGVO ││ ││ 🔍 Suche Baustelle… ││ ││ ⭐ Favoriten ││ ┌────────────────────────────┐ ││ │📍 Lehrer Allee 7, München │ ││ │ Kunde: Bgm. Müller-Schule │ ││ │ Zuletzt: heute 07:02 │ ││ └────────────────────────────┘ ││ ┌────────────────────────────┐ ││ │📍 Schulstraße 12, Köln │ ││ │ Kunde: Maier GmbH │ ││ │ Zuletzt: Fr 16.04. │ ││ └────────────────────────────┘ ││ ││ Letzte 14 Tage ││ ┌────────────────────────────┐ ││ │📍 Werkstatt Augsburg │ ││ │ Zuletzt: Mi 14.04. │ ││ └────────────────────────────┘ ││ ││ [+ Neue Baustelle anlegen] │├────────────────────────────────┤│ [Zeit] [Plan] [Doku] [Mehr] │└────────────────────────────────┘6.2 Mobile — Visitenkarten-OCR-Flow
Abschnitt betitelt „6.2 Mobile — Visitenkarten-OCR-Flow“┌────────────────────────────────┐│ ← Neuer Kunde [✕] │├────────────────────────────────┤│ 📷 Karte in Rahmen ablegen ││ ┌────────────────────────────┐ ││ │ ┌──────────────┐ │ ││ │ │ Müller GmbH │ │ ││ │ │ H. Müller │ │ ││ │ │ Tel 089/… │ │ ││ │ └──────────────┘ │ ││ │ (Live-Kamera-Vorschau) │ ││ └────────────────────────────┘ ││ ││ [📷 Auslösen] [Manuell …] │└────────────────────────────────┘
Nach Auslösen (on-device OCR ~0.4 s):
┌────────────────────────────────┐│ ← Felder prüfen [✓] │├────────────────────────────────┤│ FIRMA ││ [Müller Bauträger GmbH ]🔴││ ANSPRECHPARTNER ││ [Heinrich Müller ] ││ TELEFON ││ [+49 89 4711-23 ] ││ E-MAIL ││ [[email protected] ] ││ STRASSE ││ [Lehrer Allee 7 ]🔴││ PLZ / ORT ││ [80331] [München ]🔴││ ││ ⚠ Ähnlich: „Müller & Sohn ││ Bau KG, München" vorhanden ││ [Nur neu] [Zusammenführen] ││ ││ [Speichern als Entwurf] │└────────────────────────────────┘6.3 Mobile — Stundennachweis-Signatur vor Ort
Abschnitt betitelt „6.3 Mobile — Stundennachweis-Signatur vor Ort“┌────────────────────────────────┐│ ← Stundennachweis [⋯] │├────────────────────────────────┤│ 🛡 GoBD · §14 UStG · DSGVO ││ ││ KW 16 · 12.–18.04.2026 ││ Hannes Krüger ││ Baustelle Lehrer Allee 7 ││ ││ ┌────────────────────────────┐ ││ │ Mo 12.04. 8:00 h │ ││ │ Di 13.04. 8:30 h ⚠ 9,5h │ ││ │ Mi 14.04. Urlaub × │ ││ │ Do 15.04. 8:00 h │ ││ │ Fr 16.04. 6:15 h │ ││ │ ───────────────────────── │ ││ │ Gesamt: 30:45 h │ ││ └────────────────────────────┘ ││ ││ ┌────────────────────────────┐ ││ │ Hier unterschreiben │ ││ │ (Bauherr / Auftraggeber) │ ││ │ │ ││ │ ───── ✍️ ───── │ ││ └────────────────────────────┘ ││ [✕ Löschen] ││ ││ [✓ Signiert bestätigen] │└────────────────────────────────┘6.4 Web — Stundennachweis-Bulk-Generierung
Abschnitt betitelt „6.4 Web — Stundennachweis-Bulk-Generierung“┌────────────────────────────────────────────────────────────────────────────────┐│ Werkszeit · musterbetrieb-maler Sabine Maier · Manager [Profil ▾]│├────────────────────────────────────────────────────────────────────────────────┤│ Sidebar │ Stundennachweise · KW 16 · 12.–18.04.2026 ││ ───── │ 🛡 GoBD · §14 UStG · DSGVO ││ Zeit │ ┌──────────────────────────────────────────────────────────────┐ ││ Plan │ │ Filter: [Projekt ▼ Lehrer Allee 7] [Status ▼ alle] │ ││ ▶ Stamm │ │ [Zeitraum ▼ KW 16] [Aktualisieren] │ ││ ├ Kund. │ └──────────────────────────────────────────────────────────────┘ ││ ├ Proj. │ ││ └ Bau. │ 2 Nachweise ausgewählt · 87:30 h · Bauherr: Bgm.-Müller-Schule ││ Doku │ [An Bauherr senden] [Freigeben] [⤓ PDF herunterladen] ││ DATEV │ ┌──────────────────────────────────────────────────────────────┐ ││ │ │☑ Mitarbeiter │Zeitraum │Status │Stunden │ PDF │ ││ │ │☑ Hannes K. │KW 16 │Wartet Bauherr │42:15 │ ⤓ │ ││ │ │☑ Mehmet Y. │KW 16 │Wartet Bauherr │45:15 │ ⤓ │ ││ │ │☐ Frank D. │KW 16 │eAU │ 0:00 │ — │ ││ │ │☐ Sabine M. │KW 16 │Entwurf │40:00 │ Entwurf │ ││ │ └──────────────────────────────────────────────────────────────┘ ││ │ │└────────────────────────────────────────────────────────────────────────────────┘6.5 Web — Kunden-Vollformular mit UStID-Prüfung
Abschnitt betitelt „6.5 Web — Kunden-Vollformular mit UStID-Prüfung“┌────────────────────────────────────────────────────────────────────────────────┐│ Kunden · Müller Bauträger GmbH [Abbrechen] │├────────────────────────────────────────────────────────────────────────────────┤│ Stammdaten §14 UStG DATEV §48b EStG Kontakte ││ ═════════════════════════════════════════════════════════════════════════════ ││ FIRMA USTID ││ [Müller Bauträger GmbH ]🔴 [DE123456789 ] [Prüfen] ││ ✓ BZSt-geprüft 19.04.26 · gültig ││ STRASSE STEUERNUMMER ││ [Lehrer Allee 7 ]🔴 [143/456/78901 ] ││ PLZ ORT ││ [80331]🔴 [München ]🔴 ││ ││ DEBITORENNUMMER (DATEV) ZAHLUNGSZIEL ││ [10023 ] [14 Tage, 2 % Skonto bei 7 Tagen ▼] ││ ││ ⚠ Dieser Datensatz wurde mobil mit Minimal-Daten angelegt. Bitte ergänzen: ││ ○ UStID fehlte → ergänzt ✓ ││ ○ Debitorennummer fehlte → ergänzt ✓ ││ ○ §48b EStG-Freistellung fehlt (nur Bau-Subunternehmer) → [Foto hochladen] ││ ││ [Speichern & zurück] │└────────────────────────────────────────────────────────────────────────────────┘6.6 Web — Stundensatz-Historie (SCD Type 2)
Abschnitt betitelt „6.6 Web — Stundensatz-Historie (SCD Type 2)“┌────────────────────────────────────────────────────────────────────────────────┐│ Stundensätze · Hannes Krüger [Neuer Satz] │├────────────────────────────────────────────────────────────────────────────────┤│ EFFEKTIVER SATZ HEUTE: 52,00 €/h (Override → Mitarbeiter → Rolle „Monteur") ││ ││ Gültig ab Gültig bis Satz Scope Quelle ││ 01.01.2026 — 52,00 € Mitarbeiter-Override Admin SM ││ 01.01.2025 31.12.2025 48,00 € Mitarbeiter-Override Admin SM ││ 01.07.2024 31.12.2024 46,50 € Rolle „Monteur" System-Default││ ││ ⚠ Historische Sätze werden nicht überschrieben — jeder Satz erzeugt eine ││ neue versionierte Zeile. Abrechnungen werden zum gültigen Satz am ││ Leistungsdatum rückwirkend konsistent bleiben (GoBD-Nachvollziehbarkeit). │└────────────────────────────────────────────────────────────────────────────────┘7. Datenmodell-Skizze
Abschnitt betitelt „7. Datenmodell-Skizze“export const customersTable = pgTable('customers', { id: uuid('id').primaryKey().defaultRandom(), // UUID v7 (Idempotenz) tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), name: text('name').notNull(), legalForm: text('legal_form'), // GmbH, KG, GbR, e.K., Privat street: text('street').notNull(), zip: text('zip').notNull(), city: text('city').notNull(), country: text('country').notNull().default('DE'), vatId: text('vat_id'), // EU-USt-IdNr vatValidatedAt: timestamp('vat_validated_at', { withTimezone: true }), vatValidationResult: jsonb('vat_validation_result'), // BZSt-Antwort (Cache 24h) taxNumber: text('tax_number'), // Steuernummer (DE-Format) datevDebitorNr: text('datev_debitor_nr'), // DATEV-Debitorennummer paymentTermsDays: integer('payment_terms_days').default(14), skontoPercent: numeric('skonto_percent', { precision: 4, scale: 2 }), skontoDays: integer('skonto_days'), ibanEnc: text('iban_enc'), // pgcrypto AES-256 freistellung48bPhotoS3Key: text('freistellung_48b_photo_s3_key'), freistellung48bValidUntil: date('freistellung_48b_valid_until'), draftByField: boolean('draft_by_field').notNull().default(false), archivedAt: timestamp('archived_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), // Audit-Hash-Chain (siehe Feinkonzept §3.11) hashPrev: bytea('hash_prev'), hashSelf: bytea('hash_self'),}, (t) => ({ tenantIdx: index('customers_tenant_idx').on(t.tenantId, t.archivedAt, t.name), nameTrgm: index('customers_name_trgm_idx').using('gin', sql`${t.name} gin_trgm_ops`), draftIdx: index('customers_draft_idx').on(t.tenantId).where(sql`draft_by_field = true`),}));
export const projectsTable = pgTable('projects', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), customerId: uuid('customer_id').notNull().references(() => customersTable.id), name: text('name').notNull(), description: text('description'), status: text('status').notNull().default('angebot'), // angebot|angenommen|in_arbeit|abgeschlossen|archiviert startDate: date('start_date'), endDate: date('end_date'), budgetHours: numeric('budget_hours', { precision: 10, scale: 2 }), budgetEuro: numeric('budget_euro', { precision: 12, scale: 2 }), warnThresholdPercent: integer('warn_threshold_percent').default(80), bauleitungUserId: uuid('bauleitung_user_id').references(() => usersTable.id), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id), hashPrev: bytea('hash_prev'), hashSelf: bytea('hash_self'),}, (t) => ({ tenantIdx: index('projects_tenant_customer_idx').on(t.tenantId, t.customerId),}));
export const constructionSitesTable = pgTable('construction_sites', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), projectId: uuid('project_id').references(() => projectsTable.id), // nullable: Direkt-Zuordnung Kunde ohne Projekt customerId: uuid('customer_id').notNull().references(() => customersTable.id), label: text('label').notNull(), street: text('street').notNull(), zip: text('zip').notNull(), city: text('city').notNull(), geoLat: numeric('geo_lat', { precision: 9, scale: 6 }), geoLng: numeric('geo_lng', { precision: 9, scale: 6 }), sokaBauNr: text('soka_bau_nr'), // nur Bau-Tenants active: boolean('active').notNull().default(true), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id),}, (t) => ({ tenantIdx: index('sites_tenant_customer_idx').on(t.tenantId, t.customerId, t.active), geoIdx: index('sites_geo_idx').using('gist', sql`ll_to_earth(${t.geoLat}, ${t.geoLng})`),}));
export const hourlyRatesTable = pgTable('hourly_rates', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), scope: text('scope').notNull(), // 'role' | 'user' | 'customer_override' roleId: text('role_id'), userId: uuid('user_id').references(() => usersTable.id), customerId: uuid('customer_id').references(() => customersTable.id), rateEuro: numeric('rate_euro', { precision: 8, scale: 2 }).notNull(), validFrom: date('valid_from').notNull(), validUntil: date('valid_until'), // nullable = open-ended, SCD Type 2 createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id), hashPrev: bytea('hash_prev'), hashSelf: bytea('hash_self'),}, (t) => ({ effectiveIdx: index('rates_effective_idx').on(t.tenantId, t.userId, t.validFrom), noOverlap: check('rates_no_overlap', sql`valid_until IS NULL OR valid_until > valid_from`),}));
export const stundennachweisTable = pgTable('stundennachweis', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), userId: uuid('user_id').notNull().references(() => usersTable.id), constructionSiteId: uuid('construction_site_id').notNull().references(() => constructionSitesTable.id), periodStart: date('period_start').notNull(), periodEnd: date('period_end').notNull(), totalHours: numeric('total_hours', { precision: 8, scale: 2 }).notNull(), status: text('status').notNull().default('entwurf'), // entwurf | wartet_bauherr | signiert | versendet | abgerechnet | storniert signatureSvg: text('signature_svg'), // SVG der Touch-Unterschrift (Bauherr) signerName: text('signer_name'), signedAt: timestamp('signed_at', { withTimezone: true }), pdfS3Key: text('pdf_s3_key'), // S3 mit Object Lock (10 Jahre, §147 AO) pdfSha256: bytea('pdf_sha256'), emailedAt: timestamp('emailed_at', { withTimezone: true }), emailedTo: text('emailed_to'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id), hashPrev: bytea('hash_prev'), hashSelf: bytea('hash_self'),});RLS-Policy-Sketch (Pflicht für jede Tabelle — DOD §2.4).
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
-- Strikte Tenant-Isolation — gilt für ALLE Operations, keine Ausnahme.CREATE POLICY customers_tenant_isolation ON customers USING (tenant_id = current_setting('app.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
-- Scope-Feingranularität (Lesen): Mitarbeiter mit Scope 'own' sehen nur Kunden,-- bei denen sie als Team-Mitglied in mind. einer Baustelle beteiligt sind.CREATE POLICY customers_scope_read ON customers FOR SELECT USING ( tenant_id = current_setting('app.tenant_id')::uuid AND ( current_setting('app.scope') = 'all' OR (current_setting('app.scope') = 'team' AND EXISTS (SELECT 1 FROM construction_sites cs JOIN site_team_members stm ON stm.site_id = cs.id WHERE cs.customer_id = customers.id AND stm.user_id = current_setting('app.user_id')::uuid)) OR (current_setting('app.scope') = 'own' AND EXISTS (SELECT 1 FROM time_entries te JOIN construction_sites cs ON cs.id = te.construction_site_id WHERE cs.customer_id = customers.id AND te.user_id = current_setting('app.user_id')::uuid AND te.created_at > now() - interval '90 days')) ) );
-- Schreibschutz: Quick-Create (draft) darf jeder, Vollpflege nur Admin/Buchhaltung.CREATE POLICY customers_draft_create ON customers FOR INSERT WITH CHECK ( tenant_id = current_setting('app.tenant_id')::uuid AND draft_by_field = true );
CREATE POLICY customers_admin_update ON customers FOR UPDATE USING ( tenant_id = current_setting('app.tenant_id')::uuid AND current_setting('app.role') IN ('admin', 'buchhaltung') );Analoge Policies gelten für projects, construction_sites, hourly_rates, stundennachweis.
ER-Bezug. Referenziert tenants, users, site_team_members (aus §3.9-Feinkonzept), time_entries (aus §3.1-Feinkonzept). Wird referenziert von invoices, nachtraege, aufmass (aus Handwerks-Feinkonzepten V1).
8. API-Endpunkte
Abschnitt betitelt „8. API-Endpunkte“| Methode | Pfad | Auth-Scope | Rate-Limit | Idempotenz | Beschreibung |
|---|---|---|---|---|---|
POST |
/v1/customers |
customers:write |
Standard | Idempotency-Key Pflicht | Anlage (Quick-Create oder Vollform) |
GET |
/v1/customers |
customers:read:<scope> |
Standard | — | Liste mit Filter (?q=, ?draft=true) |
GET |
/v1/customers/{id} |
customers:read:<scope> |
Standard | — | Detail |
PATCH |
/v1/customers/{id} |
customers:write |
Standard | Idempotency-Key | Partielle Mutation (Audit-Log Pflicht) |
POST |
/v1/customers/validate-vat |
customers:write |
Privileged (10/min/Tenant) | — | BZSt-UStID-Abgleich |
POST |
/v1/customers/{id}/merge |
customers:admin |
Privileged | Idempotency-Key | Dedup-Merge (append-only, Ziel-ID bleibt) |
POST |
/v1/projects |
projects:write |
Standard | Idempotency-Key | Anlage |
GET |
/v1/projects |
projects:read:<scope> |
Standard | — | Liste |
GET |
/v1/projects/{id}/budget |
projects:read:<scope> |
Standard | — | Budget-Stand (Ist vs. Soll, Forecast) |
POST |
/v1/construction-sites |
sites:write |
Standard | Idempotency-Key | Anlage (Quick-Create auch offline) |
GET |
/v1/construction-sites/nearby |
sites:read:<scope> |
Standard | — | ?lat=,?lng=,?radius_km= — für GPS-Vorschlag Mobile |
POST |
/v1/hourly-rates |
rates:admin |
Standard | Idempotency-Key | Neuer Satz (versioniert, SCD Type 2) |
GET |
/v1/hourly-rates/effective |
rates:read |
Standard | — | ?user_id=,?date= → effektiver Satz |
POST |
/v1/stundennachweis |
stundennachweis:write |
Standard | Idempotency-Key | Generieren (erzeugt Typst-PDF, S3-Object-Lock) |
POST |
/v1/stundennachweis/{id}/sign |
stundennachweis:sign |
Privileged | Idempotency-Key | Touch-Signatur setzen (Hash-Chain-Entry) |
POST |
/v1/stundennachweis/{id}/send-email |
stundennachweis:send |
Privileged (20/h/User) | Idempotency-Key | SES-Versand |
POST |
/v1/stundennachweis/bulk-generate |
stundennachweis:bulk |
Privileged (5/min/Tenant) | Idempotency-Key | Web-Bulk-PDF, Queue via BullMQ |
OpenAPI-Schema-Skizze (gekürzt).
paths: /v1/customers: post: operationId: createCustomer x-werkszeit-scope: customers:write x-werkszeit-rate-limit: standard parameters: - name: Idempotency-Key in: header required: true schema: { type: string, format: uuid } requestBody: content: application/json: schema: oneOf: - $ref: '#/components/schemas/CustomerQuickCreate' # draftByField=true, nur 5 Felder - $ref: '#/components/schemas/CustomerFull' # alle §14-UStG-Felder responses: '201': { content: { application/json: { schema: { $ref: '#/components/schemas/Customer' } } } } '409': { description: 'Idempotency conflict' } '422': { description: 'Validation (e.g. duplicate detected without merge-token)' }
/v1/customers/validate-vat: post: operationId: validateVat x-werkszeit-scope: customers:write x-werkszeit-rate-limit: privileged # 10/min/Tenant (BZSt-Fair-Use) requestBody: content: application/json: schema: type: object required: [vatId, name, street, zip, city] properties: vatId: { type: string, pattern: '^[A-Z]{2}[0-9A-Z]+$' } name: { type: string } street: { type: string } zip: { type: string } city: { type: string } responses: '200': content: application/json: schema: type: object properties: valid: { type: boolean } validatedAt: { type: string, format: date-time } bzstRequestId: { type: string } mismatches: { type: array, items: { type: string } }Webhook-Events.
customer.created(payload enthältdraftByField-Flag)customer.updatedcustomer.merged(alt-id, neu-id)project.created,project.budget.threshold_exceededconstruction_site.created,construction_site.archivedstundennachweis.created,stundennachweis.signed,stundennachweis.senthourly_rate.created
9. Offline-Profil
Abschnitt betitelt „9. Offline-Profil“| Profil | Begründung | Mechanik |
|---|---|---|
| Voll-offline — Quick-Create Kunde/Baustelle, Favoriten, Detail-Browser, Visitenkarten-OCR, Stundennachweis erzeugen + Signatur | Der Monteur darf nie in der Keller-Baustelle an „kein Empfang“ scheitern. | Drift-SQLite hält customers, projects, construction_sites, hourly_rates, stundennachweis vollständig für die letzten 90 Tage + Favoriten. Mutationen landen in outbox_entries mit UUID-v7-Idempotency-Key. On-device OCR via ML Kit / Apple Vision (TECH-STACK §3.2). PDF wird offline via printing-Package aus Flutter client-seitig gerendert, serverseitig neu-gerendert bei Sync (Typst als Ground-Truth). |
| Best-effort-offline — UStID-Validierung | BZSt-Abgleich braucht Online-Verbindung. | UI zeigt „BZSt-Abgleich ausstehend — wird bei Netz nachgeholt“. Bis zur Validierung gilt vat_validated_at = NULL. |
| Online-only — Bulk-Stundennachweis-Generierung, SES-Versand, §48b-Foto-Upload | Queue-Worker (BullMQ) und S3-Multipart brauchen Netz; kein sinnvoller Offline-UX. | UI blockt Button bei connectivity == none mit Erklärung. |
Konflikt-Strategie. Append-only: Mutation = neuer versionierter Eintrag mit Begründungs-Feld. Konkret: wird ein Kundensatz offline auf Gerät A und online auf Gerät B geändert, syncht A später nach. Der Server vergleicht hash_prev — stimmt die Kette nicht, wird der A-Eintrag als Konflikt-Branch gespeichert, Admin bekommt ein Merge-Ticket im Web. Kein Last-Write-Wins (LESSONS-LEARNED §4).
Foto-Sync. Visitenkarten-Foto und §48b-Bescheinigung gehen per S3-Multipart über presigned URL. Drift hält nur Thumbnail + s3_key-Referenz. Limit pro Gerät: 500 MB Foto-Cache vor Sync-Erzwingung (TECH-STACK §6).
10. Compliance-Mapping
Abschnitt betitelt „10. Compliance-Mapping“10.1 GoBD (Flag gesetzt)
Abschnitt betitelt „10.1 GoBD (Flag gesetzt)“- Append-only für abrechnungsrelevante Felder:
hourly_ratesundstundennachweissind nicht mutierbar. Korrekturen erzeugen neuen Satz mitvalid_untilauf dem alten und neuemvalid_from— oder einen Storno-Nachweis mit Verweis. - Hash-Chain:
hash_prev/hash_selfaufcustomers,projects,hourly_rates,stundennachweis. Details in Feinkonzept §3.11. - Aufbewahrung: Stundennachweis-PDFs in S3 mit Object Lock (Compliance-Mode, 10 Jahre — §147 AO Abs. 3). Customer-Kernfelder (Firma, Adresse, UStID) sind Zuordnungs-Bezug zur Rechnung und bleiben min. 10 Jahre nach letzter Rechnung referenzierbar.
- Verfahrensdokumentation: Textbaustein „Stammdaten-Pflege und Stundennachweis“ in
gobd-verfahrensdoku.md(verwaltet im Compliance-Feinkonzept §3.11).
10.2 ArbZG
Abschnitt betitelt „10.2 ArbZG“— nicht zutreffend, weil dieses Feature keine Arbeitszeiten erfasst. ArbZG-Prüfung liegt im Zeiterfassungs-Feinkonzept §3.1. Stundennachweis übernimmt die bereits geprüften time_entries — werden dort ArbZG-Warnungen markiert, erscheinen sie im PDF als Zusatzzeile („Hinweis: 15.04. 10:30 h — ArbZG-Markierung gesetzt“).
10.3 VOB/B
Abschnitt betitelt „10.3 VOB/B“— teilweise. Der Stundennachweis ist kein VOB-Nachtrag und ersetzt keinen VOB-Abschlags-/Schlussrechnungsprozess. Er ist Stundenzettel-Äquivalent (§631 ff. BGB). VOB-Nachtrag liegt in Handwerks-Feinkonzept §4.2 (separat).
10.4 DSGVO (Flag gesetzt)
Abschnitt betitelt „10.4 DSGVO (Flag gesetzt)“- Art. 5 Datenminimierung: Ansprechpartner-Feld „Funktion“ ist optional. Keine Geburtstage, keine Privat-Telefonnummer — wir brauchen nur dienstliche Kontaktdaten.
- Art. 6 Rechtsgrundlage: Vertrag (Art. 6 Abs. 1 lit. b — Kunden-Daten zur Auftragsabwicklung) + berechtigtes Interesse (lit. f — Dedup-Prüfung, Betrugsvermeidung).
- Art. 17 Löschung: Kunden-Löschung ist Soft-Delete (
archived_at). Konflikt mit GoBD: Solange abrechnungsrelevante Bezüge (Rechnungen, Stundennachweise) bestehen, gilt §147 AO (10 Jahre) vor Art. 17. Gelöst wird der Konflikt durch Maskierung statt Löschung: Ansprechpartner-Name/Telefon/E-Mail werden auf Anfrage überschrieben (redacted_at+redacted_reason), Firma+Adresse+UStID bleiben für die Rechnungs-Buchhaltung erhalten. Detaillierte Mechanik siehe Feinkonzept §3.11 §10.4. - Art. 20 Datenexport: Kunden-Ansprechpartner kann eigenen Datensatz exportieren → JSON mit Feldern Name, Funktion, Telefon, E-Mail, zugeordnete Projekte (Titel, Datum, aber nicht andere Beteiligte). Implementiert im §3.11-Feinkonzept.
- Art. 30 Verarbeitungsverzeichnis: Verarbeitung „Stammdaten Kunden/Projekte/Baustellen“ mit Zweck „Auftragsabwicklung und Rechnungsstellung“, Empfänger-Kategorien „Steuerberater via DATEV-Export, Bauherr via Stundennachweis-E-Mail“.
- DSFA: Nicht Pflicht — Verarbeitung geht nicht über normale Geschäftskontakt-Pflege hinaus, keine Scoring-, keine besondere Datenkategorie gem. Art. 9.
10.5 BFSG / WCAG 2.2 AA
Abschnitt betitelt „10.5 BFSG / WCAG 2.2 AA“— nicht zutreffend, weil Stammdaten-Pflege kein öffentlich zugänglicher ESS-Flow ist (Web-Admin-Pfad hinter Auth). Die Mobile-Flows (Quick-Create, Signatur) sind durch das Zeiterfassungs-Feinkonzept flächig im A11y-Test-Harness abgedeckt — Focus-Order, Screen-Reader-Labels (Semantics-Widgets), Kontrast ≥ 4.5:1 (Design-System-Tokens --wz-text auf --wz-paper = 15.3:1).
10.6 BetrVG §87(1)6
Abschnitt betitelt „10.6 BetrVG §87(1)6“— nicht zutreffend. Stammdaten enthalten keine Mitarbeiter-Verhaltens- oder Leistungsüberwachung. Die Baustelle ist ein Kostenträger, nicht eine Tracking-Einheit.
10.7 §14 UStG (Rechnungs-Pflichtfelder)
Abschnitt betitelt „10.7 §14 UStG (Rechnungs-Pflichtfelder)“- Vollformular erzwingt vor Freigabe: Firma/Name des Leistungsempfängers, vollständige Anschrift, UStID (B2B) ODER Steuernummer, Zahlungsbedingungen. Fehlen diese, kann aus dem Kunden keine Rechnung erzeugt werden (Backend-Validator blockiert
POST /v1/invoices).
10.7 §48b EStG (Bau-Freistellung)
Abschnitt betitelt „10.7 §48b EStG (Bau-Freistellung)“- Optionales Feld
freistellung_48b_photo_s3_key+freistellung_48b_valid_until. Wenn der Kunde ein Auftraggeber ist, bei dem Werkszeit-Tenant als Subunternehmer Bauleistungen erbringt, und keine gültige Freistellung existiert → Rechnung automatisch 15 % Bauabzugssteuer. Diese Logik greift im Rechnungs-Feinkonzept, hier liegt nur die Datenhaltung.
11. Edge-Cases
Abschnitt betitelt „11. Edge-Cases“| # | Szenario | Erwartetes Verhalten |
|---|---|---|
| EC-01 | Zeitzonenwechsel (Kunde mit Adresse in Österreich) | Tenant-Zeitzone bleibt relevant (Rechnungsdatum). Customer-Adresse hat country-Feld. Für Ö = 20 % USt + MwSt-Einstellung (wird in Rechnungs-Modul gelöst). Hier nur Speicherung. |
| EC-02 | Multi-Tenant-Bleed-Versuch (User in Tenant A sucht per ID Kunde aus Tenant B) | API antwortet 404, nicht 403 (Existenz nicht offenbaren). Kein Audit-Log-Eintrag in Tenant B. Integrationstest Pflicht (DOD §2.2). |
| EC-03 | Sync-Konflikt (Kunde offline-modifiziert auf Gerät A + online auf Web) | Server detektiert Hash-Chain-Bruch, erzeugt Konflikt-Eintrag. Admin bekommt Merge-Ticket mit Diff-View. Nutzer sehen Version des Servers, bis Admin entscheidet. |
| EC-04 | Großmenge (Manager fordert Stundennachweis-Bulk für 200 Mitarbeiter × 4 Wochen = 800 PDFs) | Job landet in BullMQ-Queue, Fortschritts-Indikator im UI, fertige PDFs als ZIP per S3 presigned URL. Serverseitiges Rate-Limit 5/min/Tenant (DDoS-Schutz). |
| EC-05 | Abgelaufenes §48b-Zertifikat (Kunde-Bauleister mit abgelaufener Freistellung) | Kunde bleibt anlegbar, aber Rechnungserstellung (im Invoice-Feinkonzept) markiert automatisch +15 % Bauabzugssteuer und zeigt Compliance-Warnung „§48 EStG Abs. 1 greift — Bauabzugssteuer einbehalten“. |
| EC-06 | Plan-Limit erreicht (Starter-Plan erlaubt 50 aktive Kunden; 51. wird angelegt) | Backend-Route blockiert mit HTTP 402 + Redirect zu Upgrade-Flow. Kein Daten-Verlust (Mobile-Outbox hält Mutation, bis Tenant upgegradet — oder Admin löscht alten). |
| EC-07 | Rollen-Demotion mid-action (Admin startet Bulk-Generierung, verliert Rolle mitten drin) | BullMQ-Worker prüft Rolle bei Job-Pickup UND vor jedem einzelnen PDF-Typst-Render. Nach Demotion: Rest-Batch pausiert, Ticket an Tenant-Admin. |
| EC-08 | Visitenkarten-OCR-Fehlerkennung (Firma = “GmbH”, Name in Firmen-Feld) | User kann alle Felder vor Speichern editieren. Wenn Felder nach OCR leer, Hinweis „Manuell ergänzen“. OCR-Confidence-Score < 0.6 → Feld rot, Pflicht zur Prüfung. |
| EC-09 | Dedup-Treffer mit nur Trigram-Similarity (Firma „Maler Schmidt GmbH“ + „Maler-Schmidt GmbH“) | Server-Side Fuzzy-Query (pg_trgm % mit Threshold 0.7). Bei Treffer → UI-Dialog mit Seitenvergleich, User wählt „Zusammenführen“ (alter ID behalten, neue Felder übernommen) oder „Beide behalten“ (Flag gesetzt, Admin-Review). |
| EC-10 | UStID-Validierung BZSt-Ausfall | BZSt nicht erreichbar → Status pending, Retry-Plan (exponential backoff 5 min / 15 min / 1 h / 6 h). Admin bekommt Dashboard-Benachrichtigung nach 4 fehlgeschlagenen Retries. Rechnung darf trotzdem gestellt werden, aber mit Warnung „UStID nicht validiert“. |
12. Akzeptanzkriterien (Gherkin)
Abschnitt betitelt „12. Akzeptanzkriterien (Gherkin)“Funktionalität: Projekte, Kunden & Baustellen — Quick-Create + Stundennachweis
Hintergrund: Angenommen ein Tenant "shk-gebruder-schmidt" mit aktivem Modul-Gate "module.kern.stammdaten" Und ein Bauleiter "Thomas Schmidt" mit Rolle "Bauleiter" und Scope "team" Und eine Baustelle "Schulstraße 12, 50667 Köln" im Tenant existiert
Szenario: Happy Path — Quick-Create Baustelle im Feld, offline, dann Sync Angenommen Thomas ist mit dem Phone offline (Flight-Mode an) Wenn Thomas auf "+ Neue Baustelle" tippt Und das Formular mit "Hauptstr. 5, 50667 Köln, Kunde: Maier GmbH" füllt Und "Speichern" drückt Dann zeigt die App "Gespeichert — wird synchronisiert" an Und ein Eintrag in Drift-Outbox mit Idempotency-Key <UUID-v7> existiert Wenn Thomas' Gerät 10 Minuten später wieder online ist Dann wird die Baustelle zum Server synchronisiert Und ein Audit-Log-Eintrag "construction_site.created" mit actor="Thomas Schmidt" wird erzeugt Und die Hash-Chain der construction_sites-Tabelle ist intakt
Szenario: Grenzfall — Tenant-Hopping-Versuch Angenommen ein zweiter Tenant "musterbetrieb-maler" mit Kunde-UUID <C-X> Wenn Thomas (Tenant shk-gebruder-schmidt) versucht, GET /v1/customers/<C-X> aufzurufen Dann antwortet die API mit HTTP 404 (NICHT 403) Und es entsteht KEIN Audit-Log-Eintrag in Tenant musterbetrieb-maler Und der Multi-Tenant-Integrationstest (DOD §2.2) failt den CI-Lauf, wenn diese Assertion fehlt
Szenario: Grenzfall — DSGVO-Löschung vs. GoBD-Aufbewahrung Angenommen ein Kunde "Müller GmbH" mit 3 Stundennachweisen und 1 Rechnung aus 2024 Und der Ansprechpartner "H. Müller" fordert DSGVO Art. 17-Löschung seiner Daten Wenn die Admin-UI "Daten maskieren" drückt Dann werden die Felder name, phone, email des Ansprechpartners überschrieben (redacted_at gesetzt) Aber die Kunden-Firma, -Adresse und -UStID bleiben unverändert (GoBD §147 AO, 10 Jahre) Und ein Audit-Log-Eintrag "customer_contact.redacted" mit Begründung erscheint Und der DSGVO-Art.-20-Export für diesen Ansprechpartner ist ab sofort leer
Szenario: Grenzfall — Stundennachweis vor Ort ohne Netz, Bauherr unterschreibt, App speichert offline Angenommen Thomas ist auf Baustelle Schulstraße 12 und offline Und eine Woche mit 5 Zeit-Einträgen (total 42:15 h) im Drift-Cache liegt Wenn Thomas auf "Stundennachweis erstellen" tippt Und der Bauherr "Hr. Müller-Bgm." auf dem Touch-Pad unterschreibt Dann erzeugt die App lokal ein PDF via printing-Package Und speichert signature_svg, signer_name, signed_at in Drift Und ein Outbox-Entry "stundennachweis.sign" mit Idempotency-Key wird angelegt Wenn das Gerät wieder online ist Dann überträgt die Outbox den Eintrag Und der Server rendert das Ground-Truth-PDF via Typst Und legt es in S3 mit Object Lock (10 Jahre) ab Und setzt pdf_sha256 + pdf_s3_key Und ein Audit-Log-Eintrag "stundennachweis.signed" mit SHA-256-Footprint erscheint
Szenario: Grenzfall — Dedup-Treffer bei Visitenkarten-OCR Angenommen Sabine scannt eine Visitenkarte "Maler Schmidt GmbH, Lehrer Allee 7, München" Und in der Kunden-Tabelle existiert bereits "Maler-Schmidt GmbH, Lehrer Allee 7, München" Wenn die OCR-Felder extrahiert sind Dann prüft das Backend mit pg_trgm (Similarity > 0.7) Und zeigt einen Dialog "Ähnlicher Kunde — zusammenführen oder neu?" Und Sabine wählt "Zusammenführen" Dann bleibt die alte Kunde-ID, fehlende Felder (z.B. Telefon) werden ergänzt Und ein Audit-Log "customer.merged" mit Verweis auf OCR-Quelle erscheint Und der neue Datensatz wird NICHT erzeugt (keine Dublette)
Szenario: Grenzfall — Plan-Limit (Starter-Plan, 51. Kunde) Angenommen der Tenant "musterbetrieb-maler" hat Plan "Starter" mit Limit 50 aktive Kunden Und bereits 50 Kunden existieren mit archived_at IS NULL Wenn Sabine einen 51. Kunden anlegt Dann antwortet die API mit HTTP 402 Payment-Required Und UI zeigt Modal "Plan-Limit erreicht → Upgrade auf Business" Und die Offline-Outbox behält die Mutation für bis zu 7 Tage (nach Upgrade Auto-Submit)13. Test-Cases
Abschnitt betitelt „13. Test-Cases“13.1 Unit-Tests
Abschnitt betitelt „13.1 Unit-Tests“- U-01 Validierungsregel: UStID-Prefix passt zu
country(DE123… nur wenn country=DE, ATU… nur AT). - U-02 Dedup-Similarity: Trigram-Score zwischen zwei Namen in vordefinierter Test-Matrix.
- U-03 SCD-Type-2-Logik für
hourly_rates: neuer Satz setztvalid_untilauf vorigen Datensatz automatisch. - U-04 Property-based: 500 zufällig generierte Visitenkarten-Texte → OCR-Parser extrahiert nie leere Pflichtfelder ohne Confidence < 0.6-Markierung.
- U-05 Hash-Chain-Integrität: nach Patch wird
hash_selfkorrekt aushash_prev + canonical_rowberechnet.
13.2 Widget- / Component-Tests
Abschnitt betitelt „13.2 Widget- / Component-Tests“- W-01 Quick-Create-Form Disabled-State wenn Pflichtfelder leer.
- W-02 Golden-Test: Stundennachweis-Detail-Screen in
de-DE+ Light/Dark. - W-03 Signature-Pad Clear-Button leert Canvas und setzt State
signed=false. - W-04 Dedup-Dialog zeigt beide Datensätze in Diff-View.
13.3 Integrations-Tests (Drizzle + Testcontainers)
Abschnitt betitelt „13.3 Integrations-Tests (Drizzle + Testcontainers)“- I-01 Multi-Tenant-Isolation: Tenant A legt Kunde K an, Tenant B kann K weder per GET, PATCH, DELETE noch über Projekt-Referenz ansprechen (DOD §2.2).
- I-02 RLS Scope-own: Monteur sieht nur Kunden, mit deren Baustellen er in den letzten 90 Tagen eine Zeit-Buchung hatte.
- I-03 Outbox-Idempotenz: doppelter Idempotency-Key → 409, kein zweiter DB-Eintrag.
- I-04 Dedup-Merge: zwei Kunden mergen → alt-ID bleibt, neue Felder übernommen,
customer.merged-Audit-Eintrag mitalt_id+neu_id. - I-05 Hash-Chain zerbricht nicht bei parallelen INSERTs (Advisory Lock pro Tabelle+Tenant).
- I-06 UStID-Validierung: BZSt-Mock liefert Mismatches →
vat_validation_result.mismatchesenthält die Felder.
13.4 E2E-Tests (Patrol + Playwright)
Abschnitt betitelt „13.4 E2E-Tests (Patrol + Playwright)“- E-01 Happy Path Mobile: Hannes legt Baustelle offline an, geht online, Sync erfolgreich, Baustelle im Web sichtbar.
- E-02 Visitenkarten-OCR auf iOS Simulator mit Test-Foto → 5 Felder befüllt + Dedup-Dialog erscheint.
- E-03 Stundennachweis-Signatur Mobile + Web-Review-Freigabe.
- E-04 Bulk-Stundennachweis: Web generiert 5 PDFs, versendet per SES (Mailhog-Test), Status-Updates live via SSE.
- E-05 Visuelle Regression: Kunden-Vollformular in de-DE light/dark.
- E-06 Accessibility: Web-Kunden-Formular axe-core ohne Critical-Findings; Mobile-Quick-Create VoiceOver-Durchlauf.
13.5 Compliance-Tests
Abschnitt betitelt „13.5 Compliance-Tests“- C-01 Hash-Chain-Integrität: Mutation eines Kundensatzes in Rohform (per Drizzle ohne API-Service) bricht
hash_self-Prüfung im Verifier. - C-02 GoBD-Append-only: DELETE auf
stundennachweisscheitert mit DB-Trigger-Exception. - C-03 DSGVO-Art.-17-Maskierung: nach Redact sind Ansprechpartner-Felder leer, aber Stundennachweise der letzten 10 Jahre weiter verknüpfbar.
- C-04 DSGVO-Art.-20-Export: JSON enthält alle Felder des Ansprechpartners, keine fremden Daten.
- C-05 S3-Object-Lock-Nachweis: Versuch, ein Stundennachweis-PDF via S3-API zu löschen → 403 vor Ablauf der Retention.
13.6 Flakiness-Schutz
Abschnitt betitelt „13.6 Flakiness-Schutz“- Lokaler 20×-Re-Run der neuen Patrol-E2E grün (DOD §2.2).
14. Nicht-Ziele
Abschnitt betitelt „14. Nicht-Ziele“| Nicht-Ziel | Begründung |
|---|---|
| CRM mit Pipeline / Opportunities | Werkszeit ist kein HubSpot — Stammdaten sind Abwicklungs-Register, nicht Vertriebspipeline. |
| Bauherr-Portal (Self-Service-Login für Auftraggeber) | Hinzufügbar, aber nur mit zahlendem Referenzkunden (DOR §1.1.1). Bis dahin E-Mail-Versand ausreichend. |
| Automatische Adress-Vervollständigung via Google Places | US-Provider-Abhängigkeit, DSGVO-Eigentor. Alternative: Deutsche Post Datafactory über Partner-API — Prüfung in V2. |
| Bonitäts-Prüfung / Creditreform-Integration | Über Scope; Buchhaltung macht das extern. Optional in V2 als Add-On. |
| Fuhrpark-Management (Fahrzeug = Baustelle?) | Basis-Fahrtenbuch im Handwerks-Modul, kein eigenes Fuhrpark-Modul. |
| Multi-Currency | Phase 1 DE-Markt = EUR. Ö/CH-Tenants: offen für V2 mit FX-Modul. |
| Erweiterte GAEB-LV-Integration bei Projekt | GAEB liegt in Handwerks-Feinkonzept §4.3. Hier nur Referenz-Feld gaeb_lv_id vorbereitet. |
15. Risiken & offene Annahmen
Abschnitt betitelt „15. Risiken & offene Annahmen“| Risiko / Annahme | Impact | Wahrscheinlichkeit | Gegenmaßnahme |
|---|---|---|---|
| BZSt-UStID-SOAP-API hat Fair-Use-Rate-Limit, das wir nicht kennen | mittel | mittel | Prototyp gegen BZSt-Sandbox, eigenes Rate-Limit 10/min/Tenant + Queue bei Überschreitung |
| Visitenkarten-OCR-Qualität bei schlechtem Licht auf Baustelle | mittel | hoch | Fallback-Button „Manuell“ prominent + Confidence-Score rot markieren bei <0.6 |
| DSGVO vs. GoBD: Maskierung vs. Löschung juristisch angreifbar | hoch | niedrig | Rechtsanwalts-Konsultation eingeplant; Verfahrensdoku-Text mit Rechtsverweis §147 AO + Art. 17 Abs. 3 DSGVO (Aufbewahrungspflichten-Ausnahme) |
| Typst-PDF-Rendering-Latenz bei Bulk 200+ PDFs | mittel | mittel | Queue via BullMQ, max 5 parallele Render-Worker, Progress-SSE im UI; Last-Test vor V1 |
| Dedup-False-Positives bei häufigen Firmennamen („Schmidt GmbH“) | niedrig | hoch | Similarity-Threshold default 0.7 + Adress-Bestandteile als Tiebreaker + User-Override „Beide behalten“ |
| SCD-Type-2-Historie macht Stundensatz-UI verwirrend | mittel | mittel | UI zeigt immer nur den effektiven Satz + Verlaufs-Drawer auf Klick; Admin kann Zukunfts-Sätze planen |
16. Abhängigkeiten
Abschnitt betitelt „16. Abhängigkeiten“- Vorbedingung:
kern/09-auth-self-service(Tenant-Setup, RLS-Context-Variables, Rollen);kern/03-zeiterfassung(nur für Stundennachweis-Konsumption — die Zeit-Buchungen selbst liegen dort). - Schnittstelle zu:
kern/11-compliance-audit(Audit-Log + Hash-Chain),kern/05-rechnungen(Rechnungsstellung konsumiert Kunden + Stundennachweise),handwerk/04-bautagebuch(Baustellen-Referenz),handwerk/10-sub-vergabe(§48b-Freistellung aus Kunde). - Wird konsumiert von: praktisch allen operativen Feinkonzepten, die einen Kostenträger brauchen. Kunden/Projekte/Baustellen sind die „Foreign-Key-Hubs“ des Systems.
17. Referenzkunde-Slot
Abschnitt betitelt „17. Referenzkunde-Slot“Status: TBD
Kandidaten-Profile:
- Malerbetrieb 15–30 MA in Bayern/BW, heute Excel+DATEV-Direkt. Pain: Stundennachweis freitags 45 min pro MA, Bauherr unterschreibt erst beim nächsten Termin → Streitpotenzial.
- SHK-Betrieb 60–100 MA in NRW, hat ERP (pds/Sander & Doll), aber nur im Büro. Pain: Monteure haben keine Stammdaten im Feld, Zuordnung passiert abends am PC → fehlerhaft.
Validierungs-Fragen für das Erst-Gespräch:
- Wie viele neue Kunden/Baustellen legen Sie pro Woche an? Wie lange dauert jeweils die Anlage?
- Wie oft passiert es, dass ein Monteur auf „Sonstiges/Intern“ stempelt statt auf den richtigen Kunden?
- Wie lange dauert die Erstellung+Versand der wöchentlichen Stundennachweise heute?
- Welche Pflichtfelder in Ihrem DATEV-Debitorenstamm sind nicht-verhandelbar?
- Haben Sie Erfahrung mit BZSt-UStID-Abgleich? Wäre eine integrierte Prüfung ein Kauf-Argument?
- Wären Sie bereit, die Visitenkarten-OCR im Beta-Status für 30 Tage gegen 50 % Rabatt auf Business-Plan zu testen?
18. Senior-Berater-Empfehlung
Abschnitt betitelt „18. Senior-Berater-Empfehlung“Build-Sequenz (innerhalb dieses Features).
- Datenmodell + RLS-Policies + Multi-Tenant-Integrationstest (Woche 1). Ohne dieses Fundament ist jede weitere Zeile Code ein Hebel für Datenlecks.
- Backend-API für
customers,projects,construction_sitesinkl. Idempotency + OpenAPI-Spec (Woche 1–2). Dart-Client muss grün bauen. - Mobile Quick-Create-Flow mit Drift-Outbox (Woche 2–3). Diese eine User-Journey ist das kommerzielle Verkaufsargument — hier muss die Bedienung sitzen.
- Stundennachweis-Generator (Typst-Template + PDF-Endpoint + S3-Object-Lock) (Woche 3–4).
- Mobile-Signatur-Flow mit offline-fähigem PDF (Woche 4).
- Web-Kunden-Vollformular mit §14-UStG-Validierung + BZSt-Prüfung (Woche 5).
- Bulk-Stundennachweis-Pipeline (Queue + SES) (Woche 6 — V1, nicht MVP).
- Visitenkarten-OCR mit ML Kit / Apple Vision + Dedup-UI (Woche 6–7 — priorisieren nach Referenzkunden-Nachfrage).
- Compliance-Tests gegen Mock-BZSt + Hash-Chain-Verifier (durchgängig, nicht am Ende).
Risiko-Reihenfolge (was zuerst absichern).
- RLS + Tenant-Isolation — nicht verhandelbar. Wenn die Multi-Tenant-Integration-Tests nicht ab Tag 1 scharf sind, kann dieses Feature nie produktiv gehen.
- Append-only + Hash-Chain — Stundennachweise und Stundensätze sind GoBD-pflichtig; ein Rollback auf „Update-in-place“ wäre später ein Audit-Finding.
- Visitenkarten-OCR ist das erste Streich-Kandidat, falls Zeit knapp wird — Quick-Create manuell reicht für MVP.
Stop-the-Bus-Triggers.
- Multi-Tenant-Bleed in einem Test.
- Hash-Chain-Bruch beim Kunden-Merge.
- DSGVO-Art.-17-Anfrage scheitert an fehlender Maskierungs-Strategie (GoBD-Aufbewahrungspflicht blockiert Löschung) — muss durch die in §10.4 skizzierte Mechanik abgefangen werden, bevor das Feature live geht.
- Stundennachweis-PDF lässt sich aus S3 löschen (Object-Lock-Config fehlerhaft).
Was uns 2027 dankbar macht.
- Idempotency-Keys auf jeder Mutation ab Tag 1 — wenn Mobile-Sync irgendwann komplex wird (Konflikte, Retries, Multi-Device), ist das Fundament schon da.
- SCD-Type-2-Stundensätze ab MVP — das spart uns die „wieso ist der alte Stundennachweis jetzt mit neuem Satz gerechnet?“-Diskussion mit dem Steuerberater.
- BZSt-Validierung als eigene Queue — wenn V2 einen zweiten amtlichen Abgleich braucht (z. B. BG-Bau-Mitgliedsnummer), ist das Pattern etabliert.
Letzte Aktualisierung: 2026-04-19. Änderungen erfordern PR-Review durch Tech-Lead + Product-Owner sowie Koordination mit den Feinkonzepten §3.9 (Auth-Scopes) und §3.11 (Audit-Log-Events).
Für Entwickler — API-Endpoints20
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| GET | /v1/kern/construction-sites | bearerAuth | Baustellen auflisten |
| POST | /v1/kern/construction-sites | bearerAuth | Baustelle anlegen |
| GET | /v1/kern/construction-sites/{id} | bearerAuth | Baustelle (Detail) |
| PATCH | /v1/kern/construction-sites/{id} | bearerAuth | Baustelle ändern |
| GET | /v1/kern/customers | bearerAuth | Kunden auflisten |
| POST | /v1/kern/customers | bearerAuth | Kunden anlegen (Quick-Create oder Vollform) |
| GET | /v1/kern/customers/{id} | bearerAuth | Kunden-Detail |
| PATCH | /v1/kern/customers/{id} | bearerAuth | Kunden-Felder pflegen |
| POST | /v1/kern/customers/{id}/redact-contact | bearerAuth | DSGVO Art. 17 — Kontakt maskieren |
| GET | /v1/kern/hourly-rates | bearerAuth | Stundensätze auflisten (Historie) |
| POST | /v1/kern/hourly-rates | bearerAuth | Neuen Stundensatz anlegen (SCD Type 2) |
| GET | /v1/kern/hourly-rates/effective | bearerAuth | Effektiver Stundensatz zu Datum |
| GET | /v1/kern/projects | bearerAuth | Projekte auflisten |
| POST | /v1/kern/projects | bearerAuth | Projekt anlegen |
| GET | /v1/kern/projects/{id} | bearerAuth | Projekt (Detail) |
| PATCH | /v1/kern/projects/{id} | bearerAuth | Projekt ändern |
| GET | /v1/kern/stundennachweis | bearerAuth | Stundennachweise auflisten |
| POST | /v1/kern/stundennachweis | bearerAuth | Stundennachweis erzeugen |
| GET | /v1/kern/stundennachweis/{id} | bearerAuth | Stundennachweis (Detail) |
| POST | /v1/kern/stundennachweis/{id}/sign | bearerAuth | Touch-Signatur setzen |