GAEB-Austausch, LV-Verwaltung, Kalkulation und Preisspiegel
Feinkonzept — GAEB / Leistungsverzeichnis / Angebotswesen
Abschnitt betitelt „Feinkonzept — GAEB / Leistungsverzeichnis / Angebotswesen“Modul: Handwerk · Referenz:
FUNKTIONSUMFANG.md §4.3Cross-Links:04-mobile-aufmass.md(Aufmaß referenziert OZ aus LV) ·05-bau-abrechnung.md(Schlussrechnung aus LV + Aufmaß) ·FUNKTIONSUMFANG.md §3.5(gemeinsame Rechnungs-Engine) Status: Feinkonzept v0.1 · 2026-04-19
1. Header & Metadaten
Abschnitt betitelt „1. Header & Metadaten“feature_id: handwerk/03-gaeb-lvtitle: GAEB-Austausch, LV-Verwaltung, Kalkulation und Preisspiegelfunktionsumfang_ref: §4.3roadmap_horizont: V1plattformen: mobile: nur-Ansicht # Kalkulator arbeitet am Bildschirm, nicht in Handschuhen web: vollständig desktop: ab V1.5 bei Bedarf # Bau-Kalkulator mit Zweit-Monitorowner_rolle: Kalkulatormodul_gate_flag: module.handwerk.gaebcompliance_flags: gobd: true # LV ist Grundlage für Abrechnung, Unveränderbarkeit Pflicht arbzg: false vob: true # VOB/A §7 (eindeutige Leistungsbeschreibung), VOB/B §2, §14 dsgvo: true # Kontaktdaten Bieter / Auftraggeber bfsg: false # reiner Admin-Flow, kein ESS betrvg: false stvg: false weitere: [GAEB DA XML 3.3, StLB-Bau, DIN 276, KoSIT-Schematron (nur indirekt über X89→Rechnung)]referenzkunde: status: TBD name: "zu klären mit Sales — Kandidat: shk-gebruder-schmidt (80-MA-SHK, NRW, öffentliche AG)" quelle: Sales-Call 2026-03-12, Nachfrage nach RIB-iTWO-Importestimate_eng_tage: 68 # Parser 14 · LV-UI 14 · Kalkulation 12 · Preisspiegel 8 · Validator 8 · Tests/Migrationen 12abhängigkeiten: - kern/11-rollen-rls # Kalkulator-Rolle + RLS-Policies - kern/12-audit-log # Hash-Chain für LV-Versionen - handwerk/04-mobile-aufmass # DA11/DA12 → OZ-Rückkopplung - handwerk/05-bau-abrechnung # X84/X89 → XRechnung2. Kontext & Problem
Abschnitt betitelt „2. Kontext & Problem“2.1 Marktrealität DE-Handwerk
Abschnitt betitelt „2.1 Marktrealität DE-Handwerk“GAEB (Gemeinsamer Ausschuss Elektronik im Bauwesen) ist seit 1985 der de-facto-Standard für die elektronische Leistungsbeschreibung im deutschen Bauwesen. Die aktuelle Austausch-Norm ist GAEB DA XML Version 3.3 (ratifiziert 2019, Errata 2022). Ältere ASCII-Phasen (DA81, DA82 …, fixed-width EBCDIC-Heritage) werden parallel noch erzeugt, weil viele Ausschreibungs-Plattformen öffentlicher AG (subreport ELViS, Deutsche eVergabe, Vergabemarktplatz Hessen, TED-EU) sie verlangen. Werkszeit muss beide Welten lesen und schreiben — sonst kann ein 40-MA-Bauunternehmen in NRW einfach nicht an öffentlichen Ausschreibungen teilnehmen.
Ohne GAEB-Fähigkeit passiert heute in Handwerksbetrieben: Der Kalkulator bekommt per E-Mail eine .d83 oder .X83-Datei, öffnet sie in RIB iTWO / Nevaris / California / STREIT, kalkuliert dort, exportiert DA84 zurück. Für jedes dieser Tools ist eine eigene Lizenz (4-stellig jährlich) plus Einführung (5-stellig) fällig. Ein 25-MA-SHK-Betrieb kann sich das nicht leisten — und kann daher nicht an Ausschreibungen mit LV-Pflicht teilnehmen. Werkszeit schließt diese Lücke ohne den Anspruch, RIB iTWO in vollem Umfang zu ersetzen: Wir lesen, kalkulieren, schreiben zurück. Die 20-%-Spezialfälle (ORCA AVA, DBD-BIM-Verknüpfung, IFC-Round-Trip) bleiben bewusst außen vor.
2.2 Der Rechtsrahmen, der das erzwingt
Abschnitt betitelt „2.2 Der Rechtsrahmen, der das erzwingt“- VOB/A § 7 verlangt eindeutige und erschöpfende Leistungsbeschreibung — praktisch umgesetzt über StLB-Bau (Standardleistungsbuch Bau, gepflegt vom Gemeinsamen Ausschuss Elektronik im Bauwesen). Abweichungen davon sind begründungspflichtig.
- VOB/B § 2 regelt Änderungen und Nachträge; jeder Nachtrag muss zwingend auf eine OZ (Ordnungszahl) des Ur-LV zeigen.
- VOB/B § 14 Abs. 1 verlangt eine prüfbare Schlussrechnung — sie ist nur prüfbar, wenn Aufmaß und Abrechnung strikt der OZ-Struktur des beauftragten LV folgen.
- GoBD: Die LV-Version, auf deren Basis Auftrag erteilt wurde, ist unveränderlich zu archivieren (10 Jahre nach §147 AO). Jede Nachtrags-Version ist eine eigene unveränderliche Version, nicht ein Edit der Vorversion.
2.3 Schmerzpunkt der Alt-App
Abschnitt betitelt „2.3 Schmerzpunkt der Alt-App“In zeitapp existierte routes/quotes.js — ein freier Angebots-Editor ohne jegliche OZ-/Titel-Hierarchie und ohne GAEB-Bindung. Die LESSONS-LEARNED §5 / §11 hält das explizit fest: „Angebote werden nicht neben dem LV gebaut, sondern direkt auf die GAEB-LV-Struktur aufgesetzt.“ Die Alt-App produzierte damit doppelte Stammdaten (Angebot-Position ≠ LV-Position ≠ Rechnungs-Position), ließ Interop-Tests gegen RIB iTWO scheitern, und jeder Nachtrag musste händisch gegen das LV verglichen werden.
Werkszeit macht das explizit anders:
- LV ist die einzige Quelle der Wahrheit für Angebot, Auftrag, Nachtrag, Aufmaß und Rechnung.
- Angebots-Varianten (Haupt-/Neben-/Alternativ-Angebot) sind Varianten derselben LV-Struktur, kein paralleler Zweig.
- Jede OZ ist gegen StLB-Bau validiert; wenn nicht StLB-konform, erzwingt der Editor eine Freitext-Begründung.
- LV-Änderungen nach Auftragserteilung sind append-only — neue Nachtrags-Version mit eigener OZ-Erweiterung (z.B.
01.02.030.N1).
2.4 Erwarteter Outcome
Abschnitt betitelt „2.4 Erwarteter Outcome“| Metrik | Ist (ohne Werkszeit) | Ziel mit Werkszeit |
|---|---|---|
| Zeit bis abgegebenes Angebot ab DA83-Eingang | 3–5 Arbeitstage (Excel-Kalkulation, manuelles DA84-Schreiben) | ≤ 4 h für 300-Positionen-LV |
| Interop-Fehler beim Re-Import durch AG | ≈ 20 % (Schema-Verstöße, OZ-Lücken, fehlende KatalogNr) |
< 2 % (strikter Export + Schematron vorab) |
| Doppelte Stammdaten Angebot ↔ LV ↔ Rechnung | ~100 % (zwei Quellen) | 0 % (eine Quelle) |
| Nachtrag mit korrektem OZ-Bezug | „meistens“ | 100 % (Editor erzwingt) |
3. Personas & Rollen
Abschnitt betitelt „3. Personas & Rollen“| Rolle | Aktion | Scope | Plattform |
|---|---|---|---|
| Kalkulator | Import DA83, Kalkulation, Export DA84, Angebots-Varianten | team |
🌐 (Desktop ab V1.5) |
| Bauleitung | LV einsehen, OZ-Bezug für Nachtrag wählen, Aufmaß-Freigabe gegen OZ | team |
📱🌐 |
| Admin | StLB-Bau-Aktualisierung, Import-Validator-Regeln konfigurieren, Preisspiegel-Zugriff | all |
🌐 |
| Buchhaltung | LV als Grundlage für X86/X89-Rechnung, Kumulations-Prüfung | all |
🌐 |
| Monteur | Positionsmenge vor Ort sehen (read-only), Aufmaß erfassen (siehe §4.4) | own/team |
📱 |
| Subunternehmer (extern) | Preisspiegel nicht einsehbar (anonymisiert); sieht nur die eigene Angebots-Variante via Link | own |
🌐 |
Persona-Skizzen:
- Thomas K. (Kalkulator, 52, Bauunternehmen 80 MA, NRW) — arbeitet 80 % am Zweit-Monitor, kennt RIB iTWO, hasst „web-Apps, die bei 2.000 Positionen lahmen“. Will Keyboard-Shortcuts für Positions-Kopie, Spalten-Fix, Massen-Preis-Update.
- Sabine M. (Bauleiterin, 45, Hochbau) — muss auf der Baustelle Nachtrag gegen LV gegenhalten. Tablet im Auto, Kontext wechselt zwischen 3 Baustellen.
- Frau Keller (Einkauf, 48, SHK) — vergleicht 4 Sub-Angebote manuell in Excel, braucht Preisspiegel mit Bieter-Anonymisierung für das interne Vergabe-Gremium.
4. User-Stories
Abschnitt betitelt „4. User-Stories“US-01 [V1] Als Kalkulator möchte ich eine per E-Mail empfangene GAEB-X83-Datei importieren, damit ich nicht alle 320 Positionen aus einer PDF- Ausschreibung abtippen muss.
US-02 [V1] Als Kalkulator möchte ich Materialkosten aus meiner Artikel-Stammdaten- Tabelle auf eine Position anwenden, mit Aufschlag auf BGK/AGK/W&G, damit der EP rechnerisch nachvollziehbar und als DA84 exportierbar ist.
US-03 [V1] Als Kalkulator möchte ich drei Angebots-Varianten (Haupt/Neben/Alternativ) derselben LV-Struktur kalkulieren und als drei X83 exportieren, damit der AG zwischen Varianten wählen kann.
US-04 [V1] Als Einkauf möchte ich einen Preisspiegel über 4 Bieter sehen, sortiert nach Gesamtsumme und je OZ-Titel, damit das Vergabe-Gremium eine begründete Entscheidung treffen kann.
US-05 [V1] Als Bauleitung möchte ich in der Mobile-App die Positions-Details einer OZ einsehen (Kurztext, Langtext, Menge, EP), damit ich auf der Baustelle mit dem Bauherrn klären kann, ob der Streitpunkt im LV enthalten ist.
US-06 [V1] Als Admin möchte ich den GAEB-Import-Validator konfigurieren (Schema- Strenge, OZ-Lücken-Toleranz, StLB-Bau-Pflichtcheck), damit wir für verschiedene AG unterschiedliche Import-Strengen fahren können.
US-07 [V1.5] Als Kalkulator möchte ich die Kalkulations-Bestandteile (ME/LE/GK/SO) als Vorlage auf ähnliche OZ-Positionen massenweise übertragen können, damit ich in einem 800-Positionen-LV nicht 800-mal dieselbe Struktur eintippen muss.
US-08 [V1.5] Als Buchhaltung möchte ich aus dem beauftragten LV automatisch eine X86-Rechnungs-Skizze erzeugen, mit Aufmaß-Mengen aus §4.4, damit die Schlussrechnung die Prüffähigkeit nach VOB/B §14 Abs. 1 erfüllt.5. Funktionale Anforderungen
Abschnitt betitelt „5. Funktionale Anforderungen“5.1 Mobile App (📱)
Abschnitt betitelt „5.1 Mobile App (📱)“- F-M-01 — LV-Ansicht read-only. Baum
Los → Titel → Position → Unterpositionmit virtualisiertem Scroll (Lazy-Load pro Titel, Positionen erst beim Expandieren). Limit 10.000 Positionen. - F-M-02 — OZ-Suche. Suche nach OZ-Präfix (
01.02.*) oder Freitext im Kurztext. Offline-fähig, wenn LV zuvor im Cache. - F-M-03 — Positions-Detail. OZ, Kurztext, Langtext (HTML-gerendert), Menge, ME, EP, GP, Status (beauftragt/frei), StLB-Katalog-Nummer. Optional: Foto-Referenz aus Bautagebuch zur selben OZ.
- F-M-04 — Nachtrags-OZ wählen. Aus LV-Baum eine OZ als Parent für einen Nachtrag wählen — erzeugt nicht selbst den Nachtrag (siehe §4.2 Nachtrag), nur den OZ-Bezug.
- F-M-05 — Offline-Cache. Zuletzt geöffnetes LV wird vollständig in SQLite vorgehalten (komprimiert, Bulk-Insert-Check-summed). Kein Mobile-Edit des LV — Schreiboperationen sind Web-exklusiv.
5.2 Web-App (🌐)
Abschnitt betitelt „5.2 Web-App (🌐)“- F-W-01 — GAEB-Import. Datei-Upload (
.d81–.d89,.x81–.x89,.xml,.zipmit mehreren Dateien). Max. Upload 50 MB. MIME-Check + Magic-Byte + Schema-Validierung. - F-W-02 — LV-Editor. Split-Pane: links Baum mit Drag-&-Drop innerhalb einer Ebene; rechts Detail-Form (Kurztext, Langtext WYSIWYG, Menge, ME, Positions-Art: Standard / Eventual / Zuschlag / Grundposition / Alternativ). Keyboard-Shortcuts (
Alt+↑/↓Position verschieben,Ctrl+Dduplizieren,Ctrl+KKalkulation öffnen). - F-W-03 — OZ-Auto-Vergabe. Beim Anlegen einer Position erhält sie automatisch die nächste OZ innerhalb des Parent-Titels, gem. konfigurierter Gliederungsregel (
NN.NN.NNdefault / 4-stufig optional). - F-W-04 — StLB-Bau-Lookup. Bei Freitext-Kurztext zeigt Auto-Complete StLB-Katalog-Treffer (Version 2024.1, jährliche Updates über Admin-Upload); Übernahme-Klick füllt StLB-Nr, Standardkurztext, Standardlangtext.
- F-W-05 — Kalkulations-Overlay. Pro Position: Zeilen
Materialeinzelkosten (ME),Lohneinzelkosten (LE)(Zeit × Stundensatz),Gerätekosten (GK),Sonstige (SO). Zuschläge als %-Aufschlag:BGK(Baustellengemeinkosten),AGK(Allgemeine Geschäftskosten),W&G(Wagnis & Gewinn). Resultat: EP = ∑ME + ∑LE + ∑GK + ∑SO, dann × (1+BGK) × (1+AGK) × (1+W&G). Rundung konfigurierbar (ROUND_HALF_EVENPflicht für DATEV-Kompatibilität). - F-W-06 — Angebots-Varianten. Haupt-, Neben-, Alternativ-Angebot als Fork auf Positionsebene (Branch-Pointer), ohne Datenduplizierung für unveränderte Positionen (Copy-on-Write).
- F-W-07 — Export. Auswahl
DA81 | X81 | DA82 | X82 | DA83 | X83 | DA84 | X84 | DA85 | X85 | DA86 | X86 | DA89 | X89. Vor Export läuft der interne Schema-Check; rot → Export gesperrt, Liste der Fehler mit OZ-Tiefenbezug. - F-W-08 — Preisspiegel. Tabelle: Zeilen = OZ/Titel, Spalten = Bieter (anonymisierbar: „Bieter A/B/C“). Zeilen-Summen, Spalten-Summen, Ranking je OZ, farbige Delta-Markierung (>10 % vom Median = orange, >25 % = rot). Export CSV + PDF.
- F-W-09 — Versions-Migration. Import einer GAEB-DA-ASCII-Datei erzeugt intern eine DA-XML-3.3-Repräsentation; Export kann beides. Migrations-Protokoll je Datei zeigt Regel-Differenzen (
Vergabeart 1-stellig → 2-stellig).
5.3 Cross-Plattform (🔄)
Abschnitt betitelt „5.3 Cross-Plattform (🔄)“- F-X-01 — LV-Versions-Badge. Jede Positions-Anzeige (Mobile + Web + PDF) zeigt
v3 · beauftragt 2026-03-12aus der Version-Historie. - F-X-02 — OZ-Referenz-Resolver. Gemeinsame Logik
oz.parse('01.02.030.a')/oz.format({los:1,titel:2,pos:30,unterpos:'a'})— in Dart und TypeScript bit-identisch (sharedpackages/oz-core/, Port pro Sprache, Property-Tests auf beiden Seiten).
5.4 Admin-Konfiguration
Abschnitt betitelt „5.4 Admin-Konfiguration“- F-A-01 — StLB-Bau-Katalog hochladen. Admin lädt StLB-Bau-Katalog-ZIP (offizielle GAEB-Distribution) hoch; Parser indexiert Positionen, macht sie im Lookup-Search verfügbar.
- F-A-02 — Gliederungsregel. Default OZ-Format (
NN.NN.NN/NN.NN.NN.N/NN.NN.NNN.N) pro Tenant. Nach Anlegen eines LV nicht mehr änderbar (OZ-Migration zu schmerzhaft). - F-A-03 — Zuschlagssätze Default. BGK/AGK/W&G-Prozentsätze pro Tenant als Default, in Kalkulation editierbar. Historisiert (Stichtag).
- F-A-04 — Import-Validator-Strenge. Pro Tenant:
strict(Schema 1:1, OZ-Lückenfreiheit, StLB-Pflicht) /balanced(Schema-Validation, OZ-Lücken Warnung, StLB optional) /loose(nur Parse-Erfolg). Defaultbalanced. - F-A-05 — Export-Profile. Vorlage pro AG (z.B. „Stadt München“, „DB Netz“) — legt Export-Format, Dateiname-Schema, Zeichensatz (UTF-8 / ISO-8859-15 / Windows-1252 für Altsysteme), Gewerke-Codierung fest.
5.5 Roadmap-Schichtung
Abschnitt betitelt „5.5 Roadmap-Schichtung“| ID | MVP | V1 | V1.5 | V2 |
|---|---|---|---|---|
| F-M-01 … F-M-05 | ✅ | |||
| F-W-01 bis F-W-07 | ✅ | |||
| F-W-08 Preisspiegel | ✅ | |||
| F-W-09 Migration DA↔X | ✅ | |||
| F-X-01 LV-Versions-Badge | ✅ | |||
| F-X-02 OZ-Resolver | ✅ | |||
| F-A-01 StLB-Upload | ✅ | |||
| F-A-02 Gliederungsregel | ✅ | |||
| F-A-03 Zuschlagssätze | ✅ | |||
| F-A-04 Validator-Strenge | ✅ | |||
| F-A-05 Export-Profile | ✅ | |||
| Massenkopie von Kalk. (US-07) | ✅ | |||
| IFC-Verknüpfung BIM | ❌ (explizit Nicht-Ziel, §14) |
6. Mockups & Flows
Abschnitt betitelt „6. Mockups & Flows“HTML-Hero-Mockup: 03-gaeb-lv.html — Web dominant, Mobile als schmaler Nebendarsteller (Kalkulator-Workbench ist Web-Arbeit).
6.1 LV-Editor Web (Hauptscreen)
Abschnitt betitelt „6.1 LV-Editor Web (Hauptscreen)“┌────────────────────────────────────────────────────────────────────────────────┐│ Werkszeit · shk-gebruder-schmidt Thomas K. · Kalkulator [Profil ▾] │├────────────────────────────────────────────────────────────────────────────────┤│ Sidebar │ LV · Sanierung Schule A · 320 Positionen · X83 v3 🟡 kalkuliert ││ ───────── │ [⤒ Import DA83] [⤓ Export DA84] [🔍 Schema-Check] [+ Variante] ││ Zeit │ ┌──────────────────────────────┬─────────────────────────────────┐ ││ Plan │ │ OZ-Baum 🔍 Suche │ Position 01.02.030 │ ││ Doku │ │ ▾ 01 Rohbau │ ─────────────────────────────── │ ││ ▶ LV │ │ ▾ 02 Putzarbeiten │ Kurztext: │ ││ Aufmaß │ │ ○ 01.02.010 Putz aufbr. │ Putz-Innenwand abreißen │ ││ Nachtr. │ │ ● 01.02.030 Putz innen │ 🔴 ME: m² Menge: 128,50 │ ││ Rechn. │ │ ○ 01.02.040 Putz Decke │ ────────────────────────────── │ ││ Stamm │ │ ▸ 03 Maurerarbeiten │ EP 12,40 € GP 1.593,40 € │ ││ │ │ ▸ 02 Ausbau │ ────────────────────────────── │ ││ │ │ ▸ 03 Technische Anlagen │ Kalkulation [Ctrl+K] │ ││ │ │ ┌──────────────────────────┐ │ ME Material 1,80 € │ ││ │ │ │ Lücken: 01.02.020 fehlt │ │ LE Lohn 7,20 € (0,3h×24) │ ││ │ │ │ StLB: 3 OZ ohne Kat-Nr │ │ GK Gerät 0,40 € │ ││ │ │ └──────────────────────────┘ │ SO Sonstiges 0,00 € │ ││ │ │ │ ──────────────────── │ ││ │ │ │ Σ 9,40 € │ ││ │ │ │ +BGK 8% → 10,15 € │ ││ │ │ │ +AGK 12% → 11,37 € │ ││ │ │ │ +W&G 9% → 12,39 € ≈ 12,40 €│ ││ │ └──────────────────────────────┴─────────────────────────────────┘ ││ │ [🟢 Version 3 gespeichert · 10:42 · Thomas K.] │└────────────────────────────────────────────────────────────────────────────────┘6.2 Import-Wizard Web (Schema-Check fehlgeschlagen)
Abschnitt betitelt „6.2 Import-Wizard Web (Schema-Check fehlgeschlagen)“┌────────────────────────────────────────────────────────────────────────────────┐│ GAEB-Import · schule-a-sanierung-X83.x83 [Abbrechen]│├────────────────────────────────────────────────────────────────────────────────┤│ ① Upload ─────── ② Format erkannt ─────── ③ Validierung ────── ④ Übernehmen ││ ▲ ││ │ ││ Format: GAEB DA XML 3.3 · Phase 83 (Angebot) · 320 Positionen · 127 KB ││ ││ 🔴 5 Fehler · 🟡 12 Warnungen · 🟢 303 Positionen OK ││ ┌──────────────────────────────────────────────────────────────────────────┐ ││ │ 🔴 ERR-001 01.02.020 OZ-Lücke: zwischen 010 und 030 │ ││ │ 🔴 ERR-002 01.02.030 Kurztext > 70 Zeichen (GAEB-Regel) │ ││ │ 🔴 ERR-003 01.03 Titel ohne Positionen │ ││ │ 🔴 ERR-004 02.01.010 ME "qm" ungültig (→ erwartet "m2"/"m²") │ ││ │ 🔴 ERR-005 03.02.040.a Unterposition vor Hauptposition 040 │ ││ │ 🟡 WARN 15 Positionen StLB-Kat-Nr fehlt (Profil: balanced) │ ││ └──────────────────────────────────────────────────────────────────────────┘ ││ ││ [⤒ Datei korrigieren] [⚙ Toleranter Import (Profil: loose)] [▶ Weiter »] │└────────────────────────────────────────────────────────────────────────────────┘6.3 Preisspiegel (4 Bieter, anonymisiert)
Abschnitt betitelt „6.3 Preisspiegel (4 Bieter, anonymisiert)“┌────────────────────────────────────────────────────────────────────────────────┐│ Preisspiegel · Schule A · 4 Bieter · Anonymisierung 🟢 an │├────────────────────────────────────────────────────────────────────────────────┤│ OZ / Titel │ Bieter A │ Bieter B │ Bieter C │ Bieter D │ Δ ││──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───││ 01 Rohbau │ 48.210,- │ 45.980,-🏆│ 51.300,- │ 47.200,- │11%││ 01.01 Maurerarbeiten │ 12.450,- │ 11.800,-🏆│ 13.200,- │ 12.100,- │12%││ 01.02 Putzarbeiten │ 21.300,- │ 20.500,-🏆│ 23.100,- │ 21.900,- │13%││ 01.02.030 Putz innen │ 1.593,40 │ 1.501,70🏆│ 1.720,00 │ 1.620,00 │15%││ 02 Ausbau │ 82.100,-🏆│ 84.300,- │ 88.400,- │ 85.500,- │ 8%││ 03 Tech. Anlagen │142.800,- │138.500,-🏆│145.200,- │140.100,- │ 5%││──────────────────────────┼───────────┼───────────┼───────────┼───────────┼───││ SUMME NETTO │273.110,-🥈│268.780,-🏆│284.900,- │272.800,-🥉│ 6%││ ││ [⤓ CSV] [⤓ PDF-Vergabeempfehlung] [Namen anzeigen (Audit-Log!)] │└────────────────────────────────────────────────────────────────────────────────┘6.4 Mobile LV-Ansicht (Bauleitung auf Baustelle)
Abschnitt betitelt „6.4 Mobile LV-Ansicht (Bauleitung auf Baustelle)“┌────────────────────────────────┐│ ← Schule A · LV v3 🔍 ⋯ │├────────────────────────────────┤│ 01.02.030 ││ Putz-Innenwand abreißen ││ ────────────────────────────── ││ Menge: 128,50 m² ││ EP: 12,40 € ││ GP: 1.593,40 € ││ Status: 🟢 beauftragt ││ StLB: 015·12·030 ││ ────────────────────────────── ││ Langtext: ││ Bestehenden Kalk-Zement-Putz, ││ Dicke bis 20 mm, von massivem ││ Mauerwerk sorgfältig abschla- ││ gen. Untergrund besenrein. ││ Schutt auf Mulde. ││ ────────────────────────────── ││ [📸 Foto-Ref (Bautagebuch)] ││ [✚ Nachtrag zu dieser OZ] ││ [📐 Aufmaß zu dieser OZ] │├────────────────────────────────┤│ [Zeit] [Plan] [Doku] [Mehr] │└────────────────────────────────┘6.5 Fehler-Dialog — Export gesperrt
Abschnitt betitelt „6.5 Fehler-Dialog — Export gesperrt“┌────────────────────────────────────────────────────────────────┐│ ⚠ Export DA84 gesperrt │├────────────────────────────────────────────────────────────────┤│ Der Schema-Check des Export-Profils "Strict (VOB/A §7)" fand ││ 2 blockierende Fehler: ││ ││ 🔴 01.02.030 Kalkulation unvollständig (LE leer) ││ 🔴 03.04.010.a Unterposition referenziert fehlende Hauptpos. ││ ││ Warnungen (nicht-blockierend): ││ 🟡 12 Positionen: StLB-Kat-Nr fehlt ││ ││ [Zur Fehlerliste] [Profil wechseln] [Abbrechen] │└────────────────────────────────────────────────────────────────┘6.6 Versions-Historie (GoBD-Nachweis)
Abschnitt betitelt „6.6 Versions-Historie (GoBD-Nachweis)“┌────────────────────────────────────────────────────────────────┐│ LV-Versionen · Schule A │├────────────────────────────────────────────────────────────────┤│ v4 🟡 Entwurf 2026-04-19 10:42 Thomas K. ││ Preiskalkulation Pos. 01.02.030 angepasst (+2,40 €) ││ Hash: 4a8b2d…e9f1c3 Prev: 2c9e1a…8f3d ││ ──────────────────────────────────────────────────────────── ││ v3 🟢 Beauftragt 2026-03-12 14:18 Sabine M. ││ Auftrag X84 erhalten von Bauherr Gebr. Schmidt ││ Hash: 2c9e1a…8f3d Prev: a1b2c3…4d5e 🔒 unveränderlich ││ ──────────────────────────────────────────────────────────── ││ v2 🟢 Angebot X83 2026-02-28 09:04 Thomas K. ││ Neben-Angebot mit 8% Nachlass ││ Hash: a1b2c3…4d5e 🔒 ││ ──────────────────────────────────────────────────────────── ││ v1 🟢 Import X83 2026-02-15 11:00 (System) ││ Importiert aus schule-a-X83.x83 (127 KB) · KoSIT-grün │└────────────────────────────────────────────────────────────────┘Symbol-Konvention gem. TEMPLATE §6: 🔴 Pflichtfeld/Error, 🟡 Warnung, 🟢 OK, 🏆 günstigster Bieter, 🥈/🥉 Rang, 🔒 unveränderlich (GoBD).
7. Datenmodell-Skizze
Abschnitt betitelt „7. Datenmodell-Skizze“Pseudo-Drizzle-Schema (Postgres 17, alle Tabellen mit tenant_id + RLS).
import { pgTable, uuid, text, integer, numeric, timestamp, bytea, jsonb, index, uniqueIndex } from 'drizzle-orm/pg-core';
export const lvTable = pgTable('lv', { id: uuid('id').primaryKey().defaultRandom(), // UUID v7 für Idempotenz tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), projectId: uuid('project_id').notNull().references(() => projectsTable.id), bezeichnung: text('bezeichnung').notNull(), // "Schule A Sanierung" phase: text('phase').notNull(), // 'anfrage'|'angebot'|'auftrag'|'abrechnung' waehrung: text('waehrung').notNull().default('EUR'), ustSatz: numeric('ust_satz', { precision: 4, scale: 2 }), // 19.00 / 7.00 gliederung: text('gliederung').notNull().default('NN.NN.NN'), // OZ-Format, immutable currentVersion: integer('current_version').notNull().default(1), status: text('status').notNull(), // 'entwurf'|'angebot'|'beauftragt'|'abgerechnet'|'archiviert' createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id),}, (t) => ({ tenantProjectIdx: index('lv_tenant_project_idx').on(t.tenantId, t.projectId),}));
// Versionierung: jede Mutation erzeugt eine neue Zeile, Vorgänger bleibt unverändert.// Hash-Chain sichert GoBD-Unveränderlichkeit ab.export const lvVersionTable = pgTable('lv_version', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), lvId: uuid('lv_id').notNull().references(() => lvTable.id), version: integer('version').notNull(), // monoton steigend pro lv_id phase: text('phase').notNull(), // 'anfrage'|... quellDatei: text('quell_datei'), // z.B. "X83" quellHash: bytea('quell_hash'), // SHA-256 der Ursprungsdatei summeNetto: numeric('summe_netto', { precision: 14, scale: 2 }), notiz: text('notiz'), // Begründung Pflicht bei v>1 hashPrev: bytea('hash_prev'), // Hash-Chain-Vorgänger hashSelf: bytea('hash_self').notNull(), // SHA-256 (canonicalJSON) immutableAt: timestamp('immutable_at', { withTimezone: true }), // gesetzt bei 'beauftragt' createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull(),}, (t) => ({ uniqueVersion: uniqueIndex('lv_version_unique').on(t.lvId, t.version), tenantIdx: index('lv_version_tenant_idx').on(t.tenantId, t.createdAt.desc()),}));
// OZ-Eintrag — Titel, Position, Unterposition als einheitlicher Knoten mit `knotenTyp`.export const lvKnotenTable = pgTable('lv_knoten', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), lvVersionId: uuid('lv_version_id').notNull().references(() => lvVersionTable.id), parentKnotenId: uuid('parent_knoten_id').references((): any => lvKnotenTable.id), oz: text('oz').notNull(), // '01' | '01.02' | '01.02.030' | '01.02.030.a' ozSort: text('oz_sort').notNull(), // Null-gepolstert für Ordering: '01.02.030.0000a' knotenTyp: text('knoten_typ').notNull(), // 'los'|'titel'|'position'|'unterposition' positionsArt: text('positionsart'), // 'standard'|'eventual'|'zuschlag'|'grund'|'alternativ' kurztext: text('kurztext'), // max 70 Zeichen (GAEB-Limit) langtext: text('langtext'), // HTML erlaubt (sanitized) stlbKatalog: text('stlb_katalog'), // '2024.1' stlbNr: text('stlb_nr'), // '015.12.030' mengeneinheit: text('mengeneinheit'), // 'm2', 'm', 'Stk', 'h', 'psch' menge: numeric('menge', { precision: 14, scale: 4 }), ep: numeric('ep', { precision: 12, scale: 4 }), // Einheitspreis (brutto ohne USt) gp: numeric('gp', { precision: 14, scale: 2 }), // Generated column: ROUND(menge*ep, 2, HALF_EVEN) kalkulation: jsonb('kalkulation'), // { me, le, gk, so, bgk, agk, wug, rundung } gaebRaw: jsonb('gaeb_raw'), // Original-XML-Fragment für Round-Trip-Treue}, (t) => ({ lvSortIdx: index('lv_knoten_sort_idx').on(t.lvVersionId, t.ozSort), parentIdx: index('lv_knoten_parent_idx').on(t.parentKnotenId), uniqueOzProVer: uniqueIndex('lv_knoten_oz_unique').on(t.lvVersionId, t.oz),}));
// Angebots-Varianten — Branch-Pointer auf lv_version, mit Overrides pro Knoten.export const lvVarianteTable = pgTable('lv_variante', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), lvVersionId: uuid('lv_version_id').notNull(), name: text('name').notNull(), // 'Haupt' | 'Neben-1' | 'Alt-Zemenputz' varianteTyp: text('variante_typ').notNull(), // 'haupt'|'neben'|'alternativ' nachlassProz: numeric('nachlass_proz', { precision: 5, scale: 2 }), overrides: jsonb('overrides'), // [{knotenId, ep, menge, kurztext}]});
// Bieter-Angebot (für Preisspiegel)export const bieterAngebotTable = pgTable('bieter_angebot', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), lvVersionId: uuid('lv_version_id').notNull(), bieterName: text('bieter_name').notNull(), // intern anonymisiertAls: text('anonymisiert_als'), // 'Bieter A' quellDatei: text('quell_datei'), // 'X83' positionen: jsonb('positionen'), // [{oz, ep, gp}] summeNetto: numeric('summe_netto', { precision: 14, scale: 2 }), empfangenAm: timestamp('empfangen_am', { withTimezone: true }),});
// StLB-Bau-Katalog als Admin-Upload (pro Tenant opt-in, offizieller Katalog von GAEB)export const stlbKatalogTable = pgTable('stlb_katalog', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), version: text('version').notNull(), // '2024.1' aktiv: boolean('aktiv').notNull().default(true), katalogData: jsonb('katalog_data'), // parsed index});7.1 RLS-Policy-Sketch
Abschnitt betitelt „7.1 RLS-Policy-Sketch“ALTER TABLE lv ENABLE ROW LEVEL SECURITY;ALTER TABLE lv_version ENABLE ROW LEVEL SECURITY;ALTER TABLE lv_knoten ENABLE ROW LEVEL SECURITY;ALTER TABLE lv_variante ENABLE ROW LEVEL SECURITY;ALTER TABLE bieter_angebot ENABLE ROW LEVEL SECURITY;
CREATE POLICY lv_tenant_isolation ON lv USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY lv_scope ON lv FOR SELECT USING ( tenant_id = current_setting('app.tenant_id')::uuid AND ( current_setting('app.scope') = 'all' OR (current_setting('app.scope') IN ('team', 'own') AND project_id IN (SELECT project_id FROM project_member WHERE user_id = current_setting('app.user_id')::uuid)) ) );
-- Schreibsperre für bereits-beauftragte LV-Versionen (GoBD-Append-Only)CREATE POLICY lv_version_no_update_after_immutable ON lv_version FOR UPDATE USING (immutable_at IS NULL);
-- Preisspiegel-Sicht: Einkauf-Rolle darf Bieter-Namen lesen; Bauleitung nur anonymisiert.CREATE POLICY bieter_namen_restricted ON bieter_angebot FOR SELECT USING ( tenant_id = current_setting('app.tenant_id')::uuid AND ( current_setting('app.role') = 'einkauf' OR bieter_name IS NULL -- anonymisierte Kopie ) );7.2 ER-Bezug
Abschnitt betitelt „7.2 ER-Bezug“lv.project_id→projects.id(Baustelle)lv_knotenwird vonaufmass_zeile(§4.4) überoz+lv_version_idreferenziert — Fremdschlüssel fachlich, nicht physisch (Versions-Pivot).lv_versionwird vonrechnung.lv_version_idreferenziert (§4.5) — Schlussrechnung zeigt hart auf die beauftragte Version.lv_version.hashSelfHash-Chain ist Teil der tenant-globalen Audit-Hash-Chain (gem.kern/12-audit-log).
8. API-Endpunkte
Abschnitt betitelt „8. API-Endpunkte“Pfad-Konvention: /v1/handwerk/gaeb/…, Gate module.handwerk.gaeb.
| Methode | Pfad | Auth-Scope | Rate-Limit | Idempotenz | Beschreibung |
|---|---|---|---|---|---|
POST |
/v1/handwerk/gaeb/lv |
gaeb:write |
Standard | Idem-Key Pflicht | LV anlegen (leer oder aus Template) |
GET |
/v1/handwerk/gaeb/lv |
gaeb:read:<scope> |
Standard | — | Liste (Filter Projekt/Phase/Status) |
GET |
/v1/handwerk/gaeb/lv/{id} |
gaeb:read:<scope> |
Standard | — | Detail inkl. aktuelle Version |
POST |
/v1/handwerk/gaeb/lv/{id}/versions |
gaeb:write |
Standard | Idem-Key | Neue Version (Entwurf, basierend auf Vorgänger) |
POST |
/v1/handwerk/gaeb/lv/{id}/versions/{v}/commit |
gaeb:write |
Privileged | Idem-Key | Version einfrieren (immutable_at=NOW()) |
GET |
/v1/handwerk/gaeb/lv/{id}/versions/{v}/knoten |
gaeb:read:<scope> |
Standard | — | Baum (paginated pro Titel) |
PATCH |
/v1/handwerk/gaeb/lv/{id}/versions/{v}/knoten/{knotenId} |
gaeb:write |
Standard | Idem-Key | OZ-Knoten ändern (nur wenn nicht committed) |
POST |
/v1/handwerk/gaeb/import |
gaeb:import |
Privileged | Idem-Key | Datei-Upload DA/X81–89 |
POST |
/v1/handwerk/gaeb/export |
gaeb:export |
Privileged | Idem-Key | Export-Job (Phase, Profil) |
GET |
/v1/handwerk/gaeb/export/jobs/{jobId} |
gaeb:export |
Standard | — | Job-Status / Download-Link |
POST |
/v1/handwerk/gaeb/lv/{id}/preisspiegel |
gaeb:preisspiegel |
Standard | Idem-Key | Bieter-Angebot zufügen |
GET |
/v1/handwerk/gaeb/lv/{id}/preisspiegel |
gaeb:preisspiegel |
Standard | — | Preisspiegel-Aggregation |
POST |
/v1/handwerk/gaeb/validate |
gaeb:read |
Standard | — | Trocken-Validierung (kein Speichern) |
POST |
/v1/handwerk/admin/stlb-katalog |
admin:write |
Admin | Idem-Key | StLB-Bau-Katalog-Upload |
8.1 OpenAPI-Schema-Skizze
Abschnitt betitelt „8.1 OpenAPI-Schema-Skizze“paths: /v1/handwerk/gaeb/import: post: operationId: importGaebFile x-werkszeit-scope: gaeb:import x-werkszeit-rate-limit: privileged requestBody: required: true content: multipart/form-data: schema: type: object properties: file: { type: string, format: binary } projectId: { type: string, format: uuid } strictMode: { type: string, enum: [strict, balanced, loose] } responses: '202': description: Import in Arbeit (Parser-Job asynchron) content: { application/json: { schema: { $ref: '#/components/schemas/ImportJob' } } } '400': { description: 'Format nicht erkannt / Schema-Fehler' } '413': { description: 'Datei > 50 MB' }
components: schemas: ImportJob: type: object required: [jobId, status, phase, parsedPositionen, fehler, warnungen] properties: jobId: { type: string, format: uuid } status: { type: string, enum: [queued, parsing, validating, done, failed] } phase: { type: string, enum: [81, 82, 83, 84, 85, 86, 89] } xmlVersion: { type: string, example: "3.3" } parsedPositionen: { type: integer } fehler: type: array items: type: object properties: code: { type: string, example: "ERR-OZ-LUECKE" } oz: { type: string, example: "01.02.020" } meldung: { type: string } warnungen: { type: array, items: { type: object } }
LVKnoten: type: object required: [id, oz, knotenTyp] properties: id: { type: string, format: uuid } oz: { type: string, pattern: "^[0-9]{2}(\\.[0-9]{2})*(\\.[a-z])?$" } knotenTyp: { type: string, enum: [los, titel, position, unterposition] } kurztext: { type: string, maxLength: 70 } langtext: { type: string } mengeneinheit: { type: string, enum: [m, m2, m3, Stk, h, kg, t, psch, l] } menge: { type: number, format: double } ep: { type: number, format: double } gp: { type: number, format: double, readOnly: true } stlbNr: { type: string, nullable: true } kalkulation: { $ref: '#/components/schemas/Kalkulation' }8.2 Webhook-Events
Abschnitt betitelt „8.2 Webhook-Events“gaeb.lv.created— neues LV angelegtgaeb.lv.version.commited— Version eingefroren (Auftrag erteilt)gaeb.import.done— Import fertig (Payload: jobId, fehler, warnungen)gaeb.import.failed— Parser-Fehlergaeb.export.done— Export-Artefakt bereitgaeb.preisspiegel.updated— Bieter zugefügt/entfernt
9. Offline-Profil
Abschnitt betitelt „9. Offline-Profil“| Profil | Begründung | Mechanik |
|---|---|---|
| Best-effort-offline (Mobile Read) | Bauleitung will LV auf Baustelle einsehen, Netz nicht garantiert | LV bei letztem Öffnen vollständig in SQLite gecached, Positions-Detail offline sichtbar |
| Online-only (Web Editor + Import/Export) | Kalkulation erfolgt am Desktop; Import-Parser läuft serverseitig (Mustang-ähnlich), keine Parser-Binary im Browser | UI blockiert Import-Button bei connectivity=none, Toast „Import nur online“ |
| Online-only (Preisspiegel) | Cross-Tenant-Aggregation (nicht zutreffend hier) / schreibgeschützt bei Anonymisierung | — |
9.1 Foto- und Datei-Sync
Abschnitt betitelt „9.1 Foto- und Datei-Sync“Import-Upload: S3-Multipart über presigned URL; SQLite hält nur Job-Referenz + Status-Cache. GAEB-Quell-Datei wird nach erfolgreichem Parse in S3 mit Object Lock (Compliance, 10 Jahre) abgelegt — die Datei ist Teil der GoBD-relevanten Unterlagen.
9.2 Konflikt-Strategie
Abschnitt betitelt „9.2 Konflikt-Strategie“LV-Mutation ist server-authoritativ; Parallel-Edits durch zwei Kalkulatoren werden über optimistisches Locking abgefangen (If-Match: <lv_version.id>). Bei Konflikt: 409 mit Diff-Vorschau, User muss entscheiden (Overwrite / Abbrechen / Merge-Dialog — letzteres in V1.5). Kein Last-Write-Wins (LESSONS-LEARNED §4).
10. Compliance-Mapping
Abschnitt betitelt „10. Compliance-Mapping“10.1 GoBD
Abschnitt betitelt „10.1 GoBD“- Append-only LV-Versionen: Sobald
lv_version.immutable_at IS NOT NULL, verweigert RLS jeden UPDATE. Mutationen erzeugen eine neue Version mit Verweis auf Vorgänger. - Hash-Chain:
hash_self = SHA-256(canonicalJSON(lv_version + knoten-Baum + kalkulation)).hash_prevzeigt auf Vorgänger-Version, bildet eine LV-lokale Kette. Diese Kette wird an das globale Audit-Log-Hash-Merkle angehängt (Tages-Anker in S3). - Aufbewahrung: Quell-Dateien (DA83, X83) in S3 mit Object Lock (Compliance-Mode), 10 Jahre ab
immutable_at, gem. §147 AO. Schlüssel-Schema:s3://werkszeit-gobd/{tenantId}/gaeb/{lvId}/{version}/quelle.xml. - Nachvollziehbarkeit: Jede Mutation (Import, Knoten-Änderung, Kalkulation, Commit, Export) erzeugt einen Audit-Eintrag (
gaeb.lv.*) mit User, Zeit, Diff-Payload-Hash. - Verfahrensdokumentation: Textbaustein in
gobd-verfahrensdoku.md, Abschnitt „LV-Verwaltung“.
10.2 ArbZG
Abschnitt betitelt „10.2 ArbZG“Nicht zutreffend, weil Feature reine Kaufmännische Arbeit ist und keine Arbeitszeit-Erfassung umfasst.
10.3 VOB/B
Abschnitt betitelt „10.3 VOB/B“- §2 Abs. 5/6 (Änderungen, Nachträge): Jeder Nachtrag muss über Editor auf eine OZ des beauftragten LV (
lv_version.immutable_at IS NOT NULL) zeigen. Nachtrags-OZ erhält PräfixN<num>(z.B.01.02.030.N1), bleibt im Baum des Ur-LV erkennbar. - §14 Abs. 1 (prüfbare Schlussrechnung): Rechnung (§4.5) generiert sich zwingend aus
lv_version.id+ Aufmaß-Aggregation pro OZ; die Rechnung kann nicht mit einer anderen als der beauftragten Version verknüpft sein (FK-Check im Drizzle-Insert). - VOB/A §7 (eindeutige Leistungsbeschreibung): Export-Profil
stricterzwingt StLB-Bau-Katalog-Nr für jede Position; ohne Kat-Nr bleibt der Export gesperrt.
10.4 DSGVO
Abschnitt betitelt „10.4 DSGVO“- Art. 5 Datenminimierung: Bieter-Angebote speichern keine Personen-E-Mails im Klartext — nur Firmenname + anonymisierbares Label (
Bieter A). - Art. 6 Rechtsgrundlage: Art. 6 Abs. 1 lit. b (Vertragserfüllung — Angebot/Auftrag) + Abs. 1 lit. f (berechtigtes Interesse — Preisspiegel für Vergabe-Gremium).
- Art. 17 Löschung: Bieter-Stammdaten-Löschung über Maskierung (
bieter_name='[gelöscht]') wegen GoBD-Konflikt; Anonymisierungs-Label bleibt erhalten. - Art. 20 Datenexport: Bieter können über Portal-Link ihre eingereichte Version als X83-Download ziehen.
- DSFA: Nicht Pflicht (kein Profiling, keine systematische Bewertung natürlicher Personen).
10.5 BFSG / WCAG 2.2 AA
Abschnitt betitelt „10.5 BFSG / WCAG 2.2 AA“Nicht zutreffend für LV-Editor (kein ESS-Flow, kein öffentlicher Zugang). Mobile Read-View ist aber Teil der allgemeinen App → übernimmt die App-weiten A11y-Regeln (VoiceOver, Tab-Navigation, Kontrast ≥ 4.5:1).
10.6 BetrVG §87(1)6
Abschnitt betitelt „10.6 BetrVG §87(1)6“Nicht zutreffend: Feature ermöglicht weder Leistungs- noch Verhaltenskontrolle von Mitarbeitern.
10.7 Weitere Spezial-Compliance
Abschnitt betitelt „10.7 Weitere Spezial-Compliance“- GAEB DA XML 3.3 Schema: XSD-Validierung (offizielles Schema von GAEB-Dienststelle) als Pflicht im Import/Export-Pfad. Schematron-Regeln aus StLB-Dynamischer-Baudatenbank werden im
strict-Profil zusätzlich ausgewertet. - StLB-Bau (Standardleistungsbuch Bau): Katalog-Version pro Tenant aktivierbar, Versions-Migration (z.B. 2023.2 → 2024.1) über Admin-Dialog mit Differenz-Report.
- DIN 276 (Kostengruppierung): Optionales Tagging pro Position; nicht Pflicht für V1. In V2 als Reporting-Dimension.
- KoSIT-Schematron: Indirekt über §4.5 relevant, wenn LV-Position zur XRechnung-Position wird.
11. Edge-Cases
Abschnitt betitelt „11. Edge-Cases“| # | Szenario | Erwartetes Verhalten |
|---|---|---|
| EC-01 | OZ-Lücke im Import (01.02.010 und 01.02.030, kein 020) |
balanced-Profil: Warnung mit OZ-Liste im UI, Import läuft durch. strict-Profil: Import abgebrochen, Fix-Vorschlag „OZ 020 als Leerposition einfügen?“ |
| EC-02 | Zeichensatz-Bruch (ISO-8859-15-Datei mit Umlauten als Bytes \xFC, Parser stolpert) |
Magic-Byte-Erkennung erzwingt Charset-Dialog, User wählt manuell; intern wird nach UTF-8 konvertiert und als gaeb_raw.originalCharset gespeichert |
| EC-03 | Mitten im Import: LV-Version wird parallel committed | 409-Konflikt auf POST-Commit, User sieht Warnung „Version v3 wurde bereits beauftragt; neuer Import erzeugt v4 als Entwurf“ |
| EC-04 | Multi-Tenant-Bleed: User wechselt Tenant und versucht LV-ID von Tenant A in Tenant B zu lesen | API antwortet 404 (nicht 403), kein Audit-Log-Eintrag in Tenant A (RLS unsichtbar) |
| EC-05 | Großmenge: 15.000-Positionen-LV aus Infrastrukturprojekt | Parser-Lambda-Timeout 15 min, Chunked-Parse (Stream-XSD-Validator), UI zeigt Progressbar, Mobile-Cache auf letzte 5 MB Kompression begrenzt |
| EC-06 | OZ-Überlauf: 3-stufige Gliederung, Tenant braucht 4 Stufen | Gliederungsregel ist immutable pro LV; Warnung im UI, Workaround: Unterpositionen mit Buchstaben-Suffix (01.02.030.a) — gem. GAEB 3.3 zulässig |
| EC-07 | DA83-ASCII-Datei (fixed-width) mit Zeilenumbruch CRLF statt CR | Parser toleriert beide, korrigiert intern; gaeb_raw.normalizationApplied=['lineEndings:CRLF→CR'] |
| EC-08 | Export DA84, AG-System lehnt ab wegen Dateiname | Export-Profil definiert Dateinamen-Template ({ausschreibungsNr}-DA84.d84); Vorschau vor Download |
| EC-09 | Plan-Limit: Starter-Plan, aber Kunde versucht GAEB zu nutzen | Modul-Gate module.handwerk.gaeb nicht aktiv → Hono-Route antwortet 403 (nicht 404, weil Feature existiert), UI zeigt Upgrade-Modal |
| EC-10 | Preisspiegel-Bleed: Bauleitung versucht Bieter-Namen per API zu lesen | RLS-Policy bieter_namen_restricted filtert für Rolle ≠ einkauf die bieter_name-Spalte auf null; UI zeigt „Bieter A“ |
| EC-11 | Hash-Chain-Bruch (manueller DB-Edit im worst case) | compliance-test C-01 detektiert beim nächsten Commit-Versuch; LV-Version wird als „verdächtig“ markiert, Tech-Lead-Eskalation (Stop-the-bus-Trigger §18) |
| EC-12 | Rundungs-Drift zwischen GP-Berechnung (Dart Mobile) und Backend (TS) | Property-based Tests gegen 10.000 Zufalls-Mengen×EP, ROUND_HALF_EVEN Pflicht; Delta > 0,01 €: CI-Failure |
12. Akzeptanzkriterien (Gherkin)
Abschnitt betitelt „12. Akzeptanzkriterien (Gherkin)“Funktionalität: GAEB-LV-Import und -Export
Hintergrund: Angenommen ein Tenant "shk-gebruder-schmidt" mit aktivem Modul-Gate "module.handwerk.gaeb" Und ein Kalkulator "Thomas" mit Rolle "Kalkulator" und Scope "team" Und ein Projekt "Sanierung Schule A" ist angelegt
Szenario: Happy Path — DA-X83-Import, Kalkulation, DA-X84-Export Wenn Thomas die Datei "schule-a-X83.x83" über den Import-Wizard hochlädt Dann erkennt das System das Format "GAEB DA XML 3.3, Phase 83 (Angebot)" Und es werden 320 Positionen importiert Und 0 Fehler und maximal 15 Warnungen werden gemeldet Und ein LV der Version 1 mit Status "entwurf" ist verknüpft zum Projekt "Sanierung Schule A"
Wenn Thomas Position "01.02.030" öffnet Und die Kalkulation setzt auf ME=1,80 € · LE=7,20 € · GK=0,40 € · BGK=8% · AGK=12% · W&G=9% Dann wird der EP als 12,40 € berechnet (ROUND_HALF_EVEN) Und GP = 128,50 × 12,40 = 1.593,40 €
Wenn Thomas das LV als "X84 · Hauptangebot" exportiert Dann liefert das System eine Datei "schule-a-X84.x84" (UTF-8) Und der integrierte Schema-Check meldet 0 Fehler Und ein Audit-Log-Eintrag der Art "gaeb.lv.export" wird erzeugt (User=Thomas, Version=1)
Szenario: Grenzfall — Import-Validator-Fehler bei OZ-Lücke (strict) Angenommen der Tenant ist auf Import-Profil "strict" konfiguriert Und die Datei "schule-a-X83.x83" enthält OZ-Lücke zwischen "01.02.010" und "01.02.030" Wenn Thomas die Datei importiert Dann antwortet die API mit 422 und einer Fehlerliste inkl. Code "ERR-OZ-LUECKE" Und es wird KEIN LV angelegt Und Thomas bekommt den Vorschlag "Leerposition 01.02.020 einfügen" oder "Profil auf balanced wechseln"
Szenario: Grenzfall — Versions-Migration DA-ASCII → DA-XML 3.3 Angenommen Thomas lädt eine "schule-a.d83" (ASCII-GAEB, Version 2.0) hoch Wenn das System das Format als "GAEB DA 2.0, ASCII, Phase 83" erkennt Dann wird die Datei intern in DA-XML-3.3-Repräsentation überführt Und ein Migrations-Protokoll wird angezeigt mit den Regel-Differenzen Und beim Export kann Thomas explizit zwischen "DA84 (ASCII)" und "X84 (XML 3.3)" wählen Und in beiden Exporten ist der Round-Trip der importierten Felder verlustfrei (keine Feld-Drop-Warnung)
Szenario: Grenzfall — Konflikt bei Parallel-Edit durch zwei Kalkulatoren Angenommen Thomas und Lisa (beide Kalkulator im Team) bearbeiten LV v1 parallel Und Lisa hat bereits Pos. "01.02.030" auf EP=13,00 € geändert und gespeichert Wenn Thomas seine Änderung auf EP=12,40 € mit If-Match auf den ursprünglichen Etag PATCHt Dann antwortet die API mit 409 und einem Diff-Payload (Lisa: 13,00 · Thomas: 12,40 · Basis: 12,00) Und Thomas sieht die Auflösungs-Optionen "Lisas Wert übernehmen" / "Meinen Wert überschreiben (Begründung Pflicht)" / "Abbrechen"
Szenario: Grenzfall — Multi-Tenant-Isolation beim Import Angenommen ein zweiter Tenant "musterbetrieb-maler" Wenn Thomas (Tenant "shk-gebruder-schmidt") versucht, über die API ein LV aus Tenant "musterbetrieb-maler" per ID zu lesen Dann antwortet die API mit 404 Und es entsteht KEIN Audit-Log-Eintrag in Tenant "musterbetrieb-maler"
Szenario: Grenzfall — GoBD-Sperre nach Commit Angenommen LV v3 ist auf "beauftragt" gesetzt (immutable_at ist NOT NULL) Wenn Thomas versucht, in v3 einen Knoten zu ändern Dann antwortet die API mit 409 und Code "GOBD_VERSION_LOCKED" Und der UI zeigt den Hinweis "Version 3 ist beauftragt und unveränderlich. Soll ich Version 4 als Entwurf anlegen?" Wenn Thomas "Ja" klickt, dann wird v4 als Copy-on-Write erzeugt, hash_prev zeigt auf v313. Test-Cases
Abschnitt betitelt „13. Test-Cases“13.1 Unit-Tests (Dart + TypeScript)
Abschnitt betitelt „13.1 Unit-Tests (Dart + TypeScript)“- U-01 —
oz.parse()/oz.format()— Property-based (10.000 Zufalls-OZ):format(parse(s)) === sfür 4-stufige Gliederung - U-02 — EP-Kalkulation:
calculate(me, le, gk, so, bgk, agk, wug)vs. Referenz-Excel-Tabelle aus StLB, Abweichung < 0,005 € - U-03 —
ozSort-Generator: Liste von 1.000 OZ wird nachozSortsortiert = natürliche OZ-Reihenfolge - U-04 — Round-Trip:
parseGaebXml(file)→serializeGaebXml(model)= byte-identisch (abgesehen von Canonical-Whitespace) für 12 Muster-Dateien
13.2 Widget- / Component-Tests (Flutter)
Abschnitt betitelt „13.2 Widget- / Component-Tests (Flutter)“- W-01 — LV-Baum mit 10.000 Positionen — Render-Zeit < 200 ms Initial, Lazy-Load < 50 ms pro Titel
- W-02 — Golden-Test für LV-Mobile-Detail (de-DE, Light/Dark)
- W-03 — OZ-Search-Input: Eingabe „01.02“ filtert Baum in ≤ 80 ms
13.3 Integrations-Tests (Backend)
Abschnitt betitelt „13.3 Integrations-Tests (Backend)“- I-01 — Multi-Tenant-Isolation: Test legt 2 Tenants an; Tenant A darf LV von Tenant B nicht sehen (GET, PATCH, POST/export); 404 + kein Audit-Log in B
- I-02 — RLS-Policy
lv_scope: Kalkulator in Team X sieht keine LVs aus Team Y desselben Tenants - I-03 — Outbox-Idempotenz:
POST /v1/handwerk/gaeb/importmit identischem Idem-Key → 200 (zweite Antwort), nur 1 Job angelegt - I-04 — GoBD-Sperre: PATCH auf Knoten einer committed-Version → 409
GOBD_VERSION_LOCKED, kein DB-Write
13.4 E2E-Tests
Abschnitt betitelt „13.4 E2E-Tests“- E-01 — Patrol/iOS + Android + Playwright/Chromium/Firefox/WebKit: Happy-Path (Import X83 · Kalkulieren Pos 030 · Export X84) — Gherkin Szenario 1:1
- E-02 — Offline LV-Ansicht Mobile: Device offline, zuletzt geladenes LV bleibt sichtbar, OZ-Suche funktioniert
- E-03 — Visuelle Regression: Golden-Image LV-Editor Web (Light/Dark/de-DE) — Abweichung > 0,1 % blockt Merge
- E-04 — A11y: axe-core auf LV-Editor — keine Critical/Serious; Flutter-Semantics für Mobile-Detail grün
13.5 Compliance-Tests (apps/api/test/compliance/)
Abschnitt betitelt „13.5 Compliance-Tests (apps/api/test/compliance/)“- C-01 — Hash-Chain-Integrität: manueller Eingriff in
lv_version.summeNettolässt nachfolgendehash_selfbrechen; Detection-Test schlägt Alarm - C-02 — GAEB-Interop: Round-Trip mit 12 Referenz-Dateien aus RIB-iTWO-Export-Samples,
strict-Profil; Diff muss leer sein (modulo Whitespace und Timestamp-Feldern) - C-03 — StLB-Bau-Katalog-Validator: 100 Stichproben-Positionen, Katalog-Nr muss auflösbar sein
- C-04 — Schema-Check XSD: offizielles GAEB-DA-XML-3.3-XSD gegen jeden Export; 0 Validation-Errors
- C-05 — Rundungs-Property-Test: 100.000 Zufalls-
(menge, ep)—gpidentisch zwischen Dart-Client und TS-Backend, ROUND_HALF_EVEN
13.6 Flakiness-Schutz
Abschnitt betitelt „13.6 Flakiness-Schutz“- Lokaler 20×-Re-Run der neuen E2E-Tests grün (DOD §2.2 letzter Punkt)
14. Nicht-Ziele
Abschnitt betitelt „14. Nicht-Ziele“| Nicht-Ziel | Begründung |
|---|---|
| Vollständiger RIB-iTWO-Ersatz | Nicht in Phase 1. Werkszeit deckt 80 % der AVA-Aufgaben ab (Import/Kalk/Export). Komplexe BIM-Integration, CAD-Mengensplit, Bauzeiten-Simulation bleiben bei Spezial-Tools. |
| IFC-/BIM-Round-Trip | Kein Referenzkunde in Phase 1. Verschoben auf V3, dann separates Feinkonzept handwerk/12-bim-ifc.md. |
| Ausschreibungs-Plattform-Direktanbindung (subreport ELViS, Deutsche eVergabe) | API-Anbindung an öffentliche Plattformen ist pro Plattform eigener Vertrag + Zertifizierung. Werkszeit bleibt bei Datei-Up/Download; Direktanbindung frühestens V2 bei zahlendem Design-Partner. |
| ORCA AVA-Import | ORCA nutzt GAEB plus proprietäre Extensions. Kein aktueller Kunde. V2 falls nachgefragt. |
| Bauzeitenplan (Gantt) aus LV | Separates Feature (handwerk/13-bauzeitenplan.md, nicht in V1). |
| Dynamische Preisanpassung (Stoffpreisgleitklausel nach BGB §313) | Wird im Kalkulations-Editor als Metadaten-Feld geführt, aber nicht automatisch berechnet — in V2. |
| Elektronische Signatur für Angebots-Abgabe | QES/FES ist öffentliche-AG-Thema, liegt bei ELViS / Vergabeplattform. Werkszeit erzeugt nur die X83-Datei. |
| Multi-Language LV (Deutsch/Englisch parallel) | Nicht Zielmarkt Phase 1. |
15. Risiken & offene Annahmen
Abschnitt betitelt „15. Risiken & offene Annahmen“| Risiko / Annahme | Impact | Wahrscheinlichkeit | Gegenmaßnahme |
|---|---|---|---|
| GAEB-XSD-Interop mit RIB iTWO 2024.x ist strikter als unser Parser — Exports werden abgelehnt | hoch | mittel | Early-Prototype Monat 2: Export gegen iTWO-Import testen mit Muster-LV (3 Größen); evtl. Schematron-Profil „iTWO-kompatibel“ einbauen |
| StLB-Bau-Katalog-Lizenz: Verteilung an Kunden könnte Lizenz-Zahlung an GAEB-Dienststelle erfordern | mittel | mittel | Rechtsklärung vor V1-Release; Fallback: Kunde lädt eigenen Katalog hoch (Admin-Upload), Werkszeit liefert ihn nicht mit aus |
| Parser-Performance bei 15.000-Positionen-LV: Lambda-Timeout, Browser-Mobile-Cache-Overflow | mittel | niedrig | Streaming-Parser (SAX-basiert) in Node statt DOM; Lazy-Load im Client; Mobile-Cache per Titel (nicht pro LV) |
| OZ-Format-Änderung mid-lifecycle — Kunde will nachträglich 4-stufig | niedrig | hoch | Dokumentiertes Nicht-Ziel; Migration nur via LV-Neuanlage + manuellem Copy |
| Hash-Chain-Bruch durch DB-Restore | hoch | niedrig | Pre-Commit-Hook im Backup: Hash-Chain-Prüfung nach Restore Pflicht; dokumentierter Runbook |
| Mustang-Lambda-Ökosystem-Reife für X86/X89-Bezug zur Rechnung (siehe §4.5) | mittel | niedrig | Prototyp vor V1-Freeze (siehe ADR-0002) |
16. Abhängigkeiten
Abschnitt betitelt „16. Abhängigkeiten“- Vorbedingung:
kern/11-rollen-rls(Kalkulator-Rolle, RLS-Baseline),kern/12-audit-log(Hash-Chain-Infrastruktur) - Schnittstelle zu:
handwerk/04-mobile-aufmass.md— Aufmaß greift auflv_knoten.ozeinerlv_version_id(beauftragt) zuhandwerk/05-bau-abrechnung.md— X86/X89-Export-Routinen übernehmen die gleiche Mustang-Lambda-Pipeline- Projekt/Kunden-Stammdaten (Kern) — LV.projectId → projects
- Wird konsumiert von:
- Nachtrag-Management (§4.2)
- Schlussrechnung (§4.5)
- Preisspiegel-Dashboard (ist selbst Teil dieses Feinkonzepts)
17. Referenzkunde-Slot
Abschnitt betitelt „17. Referenzkunde-Slot“Status: TBD — zu klären mit Sales / PO.
DOR §1.1.1 verlangt: „Mindestens ein zahlender Design-Partner ist namentlich dokumentiert, der das Feature konkret fordert.“
Kandidaten-Profile:
- 30–80-MA-Bauunternehmen (Hochbau / SHK / Elektro), das regelmäßig an öffentlichen AG teilnimmt (Stadt, Land, DB, Bundeswehr)
- Betrieb mit ≥ 1 Vollzeit-Kalkulator/-in, der heute RIB iTWO oder Nevaris nutzt und die Kosten (4–10 k€/Jahr/Arbeitsplatz) drücken will
- Betrieb mit ausreichender IT-Reife (eigene Mail-Domain, DATEV-Steuerberater, existierende digitale Zeiterfassung)
Validierungs-Fragen für das Erst-Gespräch:
- Wie viele GAEB-Dateien bekommen Sie pro Monat, wie groß sind die (Positionen)?
- Welches Tool nutzen Sie heute — und was fehlt Ihnen daran am meisten?
- Haben Sie Probleme bei der X84-Abgabe, die der AG ablehnt? Welche Fehler kamen zurück?
- Wäre ein vereinfachtes Werkszeug (80 % der Funktionen, aber aus einem Guss mit Ihrer Zeiterfassung/Aufmaß/Rechnung) ein Kaufanreiz — bei welcher Preismarge pro Arbeitsplatz?
- Bereit für 30-Tage-Beta mit 2 echten LVs zum Import und einem Probe-Export an einen internen Test-AG?
18. Senior-Berater-Empfehlung
Abschnitt betitelt „18. Senior-Berater-Empfehlung“18.1 Build-Sequenz
Abschnitt betitelt „18.1 Build-Sequenz“- Datenmodell + RLS + Multi-Tenant-Test (immer zuerst — alles andere ist auf Sand gebaut). Inkl.
oz.parse/formatinpackages/oz-core(Dart + TS). - GAEB-Parser-Lambda (Node, Streaming-XSD-Validator, DA-ASCII → DA-XML-3.3-Adapter). Start mit X83/X84 (80 % der Praxisfälle), X81/X82/X85/X86/X89 folgen innerhalb V1.
- Backend-API + OpenAPI-Spec → Dart-Client-Generierung.
POST /importasync (Queue + Webhook),GET /jobsfür Status. - LV-Editor Web — virtualisierter Baum (ag-grid oder eigener mit
react-virtual-Äquivalent in Flutter-Web), Split-Pane, Keyboard-Shortcuts. - Kalkulations-Overlay mit
ROUND_HALF_EVEN-Pflicht auf beiden Seiten. - Mobile Read-View mit SQLite-Cache.
- Preisspiegel (Bieter-Upload, Aggregation, Anonymisierung, PDF/CSV).
- Import-Validator-Strenge (strict/balanced/loose).
- Compliance-Tests vollständig (C-01 bis C-05) — gate vor V1-Freeze.
- E2E-Suite + Doku-PR mit auto-generierten Screenshots.
18.2 Risiko-Reihenfolge (wenn Zeitnot)
Abschnitt betitelt „18.2 Risiko-Reihenfolge (wenn Zeitnot)“- Multi-Tenant-Isolation, Hash-Chain, Schema-Validator = nicht verhandelbar.
- Zuerst einsparbar: Preisspiegel-Anonymisierungs-Feinheiten (V1.5), ASCII-Import DA85/86/89 (X-Pendants zuerst), Keyboard-Shortcuts über Basis hinaus.
18.3 Stop-the-Bus-Triggers
Abschnitt betitelt „18.3 Stop-the-Bus-Triggers“- Multi-Tenant-Bleed in irgendeinem Test.
- Hash-Chain-Bruch in C-01.
- GAEB-Round-Trip-Verlust von Feldern in C-02 (schlägt für 1 Muster-Datei fehl).
- RIB-iTWO-Interop: unsere X84-Datei wird vom echten iTWO-Import abgelehnt mit Schema-Fehler, den wir nicht reproduzieren können.
18.4 Was uns 2027 dankbar macht
Abschnitt betitelt „18.4 Was uns 2027 dankbar macht“- OZ-Core als eigenes Paket (Dart + TS Port): diese winzige Investition zahlt sich bei Aufmaß (§4.4), Rechnung (§4.5), Nachtrag (§4.2) und später Bauzeitenplan als Plattform-Vorteil zurück — und hält uns von
01.02.030 != 01.02.30 != 1.2.30-Bugs. - Copy-on-Write-Varianten statt Duplikat-Tabelle: Storage-schonend, Diffs sind Gratis.
- Streaming-Parser: 15.000-Positionen-LV ist damit technisch ein Non-Issue — Tier-2-Infrastruktur-Kunden kommen ohne Refactor.
- Append-only Versionen mit Hash-Chain: sauberer GoBD-Nachweis vom ersten Tag; spart uns die Audit-Panik, wenn Finanzamt 2029 für die Projekte von 2026 Prüfung anberaumt.
Letzte Aktualisierung: 2026-04-19. Änderungen erfordern PR-Review durch Tech-Lead + PO.
Für Entwickler — API-Endpoints19
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| GET | /v1/handwerk/gaeb-lv/stlb-katalog/lookup | bearerAuth | STLB-Katalog-Lookup |
| GET | /v1/handwerk/lv | bearerAuth | Leistungsverzeichnisse auflisten. |
| POST | /v1/handwerk/lv | bearerAuth | Leistungsverzeichnis anlegen. |
| DELETE | /v1/handwerk/lv/{id} | bearerAuth | Leistungsverzeichnis löschen (nur Status `entwurf`). |
| GET | /v1/handwerk/lv/{id} | bearerAuth | Leistungsverzeichnis (Detail inkl. Status-Historie). |
| PUT | /v1/handwerk/lv/{id} | bearerAuth | Leistungsverzeichnis aktualisieren. |
| POST | /v1/handwerk/lv/{id}/export-reb23003 | bearerAuth | LV als REB-23.003-Datei exportieren (windows-1252). |
| POST | /v1/handwerk/lv/{id}/export-xml | bearerAuth | LV als GAEB-XML exportieren. |
| GET | /v1/handwerk/lv/{id}/gliederungen | bearerAuth | LV-Gliederungsebenen auflisten. |
| POST | /v1/handwerk/lv/{id}/gliederungen | bearerAuth | LV-Gliederungsebene anlegen. |
| DELETE | /v1/handwerk/lv/{id}/gliederungen/{gId} | bearerAuth | LV-Gliederungsebene löschen. |
| PUT | /v1/handwerk/lv/{id}/gliederungen/{gId} | bearerAuth | LV-Gliederungsebene aktualisieren. |
| POST | /v1/handwerk/lv/{id}/import-xml | bearerAuth | GAEB-XML in LV importieren. |
| GET | /v1/handwerk/lv/{id}/positionen | bearerAuth | LV-Positionen auflisten. |
| POST | /v1/handwerk/lv/{id}/positionen | bearerAuth | LV-Position anlegen. |
| DELETE | /v1/handwerk/lv/{id}/positionen/{pId} | bearerAuth | LV-Position löschen. |
| PUT | /v1/handwerk/lv/{id}/positionen/{pId} | bearerAuth | LV-Position aktualisieren. |
| GET | /v1/handwerk/lv/{id}/summen | bearerAuth | LV-Summen (Netto/Brutto, Anzahl Positionen/Ebenen). |
| POST | /v1/handwerk/lv/{id}/transition | bearerAuth | LV-Status wechseln. |