Mobiles Lager & Material-Management — Fahrzeug-Tag, Scan, OCI-Punchout
Feinkonzept — Mobiles Lager & Material-Management (§4.6)
Abschnitt betitelt „Feinkonzept — Mobiles Lager & Material-Management (§4.6)“Senior-Consultant-Einordnung. Dieses Feinkonzept macht das Service-Fahrzeug zum buchhalterisch korrekten Lagerort. Wir bewegen uns damit direkt im GoBD-Pfad (jede Entnahme ist ein Warenbewegungs-Beleg) und im §13b-UStG-Pfad (Material-Beistellungen an Bauleistungs-Subunternehmer). Wer hier „Last-Write-Wins“ macht oder einen UUID-v4-Key verwendet, baut sich einen Audit-Befund. Wir wählen ausdrücklich CRDT-artige Mengen-Semantik + Server-autoritative Resolution, weil Offline-Doppelentnahme in diesem Modul nicht die Ausnahme, sondern der Normalfall ist.
1. Header & Metadaten
Abschnitt betitelt „1. Header & Metadaten“feature_id: handwerk/06-mobiles-lager-materialtitle: Mobiles Lager & Material-Management — Fahrzeug-Tag, Scan, OCI-Punchoutfunktionsumfang_ref: §4.6roadmap_horizont: V2plattformen: mobile: vollständig web: vollständig mit-Bulk desktop: ausowner_rolle: Mitarbeiter # Entnahme | Manager (Bestellung, Freigabe) | Admin (Stammdaten, Händler-Integration) | Einkauf (Rahmenverträge)modul_gate_flag: module.materialcompliance_flags: gobd: true arbzg: false vob: false dsgvo: true bfsg: false # kein ESS-Flow — reine Monteur-/Manager-Funktion betrvg: false # keine Leistungs-/Verhaltenskontrolle, Geofencing ist off stvg: false weitere: [§13b UStG, §147 AO, KI-VO Art. 50, ElektroG, ProdHaftG]referenzkunde: status: TBD name: "zu klären mit Sales — siehe DOR §1.1.1 (Kandidat: SHK-Gebrüder-Schmidt / ELKO-Service Köln)" quelle: Sales-Hypothese (noch kein Discovery-Call)estimate_eng_tage: 48 # MVP-Slice: Tag + Scan + manuelle Entnahme + Server-Sync; OCI und Auto-Replenishment je +12abhängigkeiten: - handwerk/05-bau-abrechnung # Materialentnahme schreibt auf Kostenstelle Projekt (Cross-Link §4.5) - kern/02-auth-scopes # Scope `own` auf eigene Entnahmen, `team` auf Kolonne, `all` auf Tenant - kern/08-audit-log # jede Bestandsmutation ist ein Audit-Event2. Kontext & Problem
Abschnitt betitelt „2. Kontext & Problem“Marktrealität (DE-Handwerk). Das Service-Fahrzeug eines Heizungsbauers, Elektrikers oder SHK-Technikers ist operativ das tatsächliche Lager. Heute läuft das in 80 % der Betriebe so: Morgens wird ein Zettel ausgedruckt oder gar nichts, mittags wird am Großhändler nachgekauft (Würth, Hagebau, Sonepar) und der Lieferschein landet bestenfalls in der Werkstatt. Der Monteur weiß selbst nicht sicher, ob er noch 3 oder 30 Winkel DN 15 im Wagen hat. Rechnungsseitig ist das eine Katastrophe: Material wird am Ende des Projekts „nachgetragen“, also aus Erinnerung geschätzt — mit entsprechender Marge-Erosion.
Rechtlicher Rahmen:
- GoBD verlangt seit 2015, dass Warenbewegungen als Belege zeitnah und unveränderbar dokumentiert werden (§145 AO, §147 AO). Eine händisch geführte Excel-Datei erfüllt das explizit nicht — der BMF-Erlass vom 28.11.2019 nennt „Bestandsveränderungen“ als Pflicht-Nachweis.
- §13b UStG greift bei Bauleistungen: Stellt der Betrieb einem Subunternehmer Material bei, ist die Abgrenzung zwischen „Materialgestellung“ und „Einkaufsnebenleistung“ umsatzsteuerlich relevant — ohne Ausgabe-Beleg ist das nicht prüfbar.
- §147 AO verlangt 10 Jahre Aufbewahrung der Belege — unveränderbar.
- ElektroG und ProdHaftG machen eine Chargen-Rückverfolgung bei Elektro- und Sanitär-Bauteilen teilweise verbindlich (WEEE-Rücknahme, Produkthaftungs-Ausforschung bei Mangelschäden).
Schmerzpunkt der Alt-App. FahrzeugLagerScreen und MaterialVerbrauchScreen aus der zeitapp sind Teil der Route-Wildwuchs-Diagnose in LESSONS-LEARNED §2 — beide Screens existierten, aber ohne Barcode-Scan, ohne OCI, ohne Offline-Konflikt-Strategie und ohne tenant-seitige Bestandsführung im Postgres-Sinn. Ein gleichzeitiger Entnahme-Konflikt zweier Monteure führte im schlimmsten Fall zu Negativ-Beständen, die niemand reparierte. Genau den Punkt adressieren wir hier (siehe LESSONS-LEARNED §4 — „Offline-First war nur halb offline“).
Erwarteter Outcome.
- Material-Erfassung 80 % automatisiert: Scan statt Suche, 3 Sekunden pro Entnahme statt 3 Minuten pro Tag in der Werkstatt.
- Bestell-Durchlaufzeit −60 %: OCI-Punchout direkt aus der App statt abendliches Fax an den Großhandel.
- Material-Marge +3 bis +5 Prozentpunkte: weil Material direkt auf das Projekt kontiert wird und nicht pauschal in „Regie“ verschwindet (siehe §4.5 Bau-Abrechnung).
- Inventur-Aufwand von 2 Personentagen auf 2 Stunden: dank Stichtags-Snapshot + mobiler Scan-Inventur.
3. Personas & Rollen
Abschnitt betitelt „3. Personas & Rollen“| Rolle | Aktion | Scope | Plattform |
|---|---|---|---|
| Mitarbeiter (Monteur) | Fahrzeug morgens per NFC anmelden; Material per Scan entnehmen / rückbuchen; Mindestbestand-Warnung erhalten; Punchout starten; Inventur-Zählung ausführen | own auf eigene Entnahmen, team auf das aktuelle Fahrzeug |
📱 (Web nur Nachkontrolle) |
| Manager | Bestellvorschläge freigeben; Budget-Freigabe bei OCI-Cart über Limit; Fehler-Tickets aus Konflikten bearbeiten; Material-Ist-Kosten pro Projekt prüfen | team |
🌐 primär, 📱 Approve-Push |
| Admin | Händler-Integration OCI einrichten; Artikel-Stammdaten pflegen; Soll-Bestände pro Fahrzeug-Profil; NFC-Tags onboarden; Auto-Replenishment-Regeln | all |
🌐 |
| Einkauf | Rahmenverträge pflegen; Preislisten-Import (BMEcat 2005 / GAEB XML); Lieferanten-Matching | all |
🌐 |
| Buchhaltung | GoBD-Export der Bestandsmutationen; Inventur-Differenz-Bericht; §13b-Material-Beistellungs-Liste | all |
🌐 |
Persona-Skizzen.
- Mehmet Yılmaz (Monteur SHK, 34, musterbetrieb-maler / shk-gebruder-schmidt) — startet 06:55 Uhr, tippt NFC-Fahrzeug-Tag am Armaturenbrett an, die App springt auf „Fahrzeug M-SG 247 · Bestand 412 Positionen“. Im Heizungskeller hat er keinen Empfang, aber Handschuhe und eine Stirnlampe. Er scannt die EAN auf dem Schraubenkarton → „Schraube DIN 7991 M8x40 Edelstahl A2, 47 Stk vorhanden → 10 entnommen“. Wenn die 20er-Schwelle unterschritten wird, erscheint morgen früh automatisch eine Nachbestell-Karte.
- Thomas Schmidt (Bauleiter, 48, shk-gebruder-schmidt) — sieht im Web-Dashboard, dass Fahrzeug M-SG 247 heute 184 € Material für Projekt „BV Lehrer Allee 7“ verbucht hat, genehmigt per Klick einen Würth-Cart über 620 € und bekommt eine Konflikt-Warnung „Negativ-Bestand DN-15-Winkel, 2 parallele Entnahmen, Auto-Nachbestellung ausgelöst“.
- Sabine Maier (Admin, 45, musterbetrieb-maler) — konfiguriert OCI-Punchout-URL zum Hagebau-Portal inkl. HMAC-Secret und testet den Round-Trip im Sandbox-Mandanten.
- Petra Albrecht (Buchhaltung, 58, externer Steuerberater) — zieht am Monatsende den GoBD-Export der Bestandsbewegungen und die Inventur-Differenz-Liste.
4. User-Stories
Abschnitt betitelt „4. User-Stories“US-01 [V2 MVP] Als Monteur möchte ich mein Fahrzeug per NFC-Tag anmelden, um morgens in unter 2 Sekunden ohne Tippen im richtigen Lager-Kontext zu sein.
US-02 [V2 MVP] Als Monteur möchte ich Material per GS1-128-Scan entnehmen, damit ich auch mit Handschuhen und ohne Empfang eine korrekte Buchung erzeuge.
US-03 [V2 MVP] Als Monteur möchte ich bei Unterschreitung des Mindestbestands eine Bestellvorschlag-Karte mit einem Tap zur Freigabe erhalten.
US-04 [V2 MVP] Als Manager möchte ich Bestellungen an den Großhändler per OCI-Punchout auslösen, damit der Warenkorb aus dem Händler-Portal 1:1 als Bestellung in Werkszeit liegt.
US-05 [V2 MVP] Als Manager möchte ich beim Offline-Doppel-Entnahme-Konflikt automatisch ein Lieferverzug-Ticket inkl. Alternativ-Fahrzeug-Vorschlag sehen.
US-06 [V2] Als Admin möchte ich eine Stichtags-Inventur mit Scan-Zählung und Differenz-Bericht auslösen, um GoBD-konforme Korrekturbuchungen vorzunehmen.
US-07 [V2] Als Buchhaltung möchte ich eine §13b-Material-Beistellungs-Liste pro Projekt exportieren, um die Reverse-Charge-Abgrenzung sauber zu dokumentieren.
US-08 [V2.5] Als Einkauf möchte ich Rahmenverträge mit Staffelpreisen so hinterlegen, dass der Punchout automatisch die richtige Konditionen-ID sendet.
US-09 [V2.5] Als Admin möchte ich einen Auto-Replenishment-Modus aktivieren, der wöchentlich aus Soll-Bestand minus Ist-Bestand automatisch einen Cart befüllt.5. Funktionale Anforderungen
Abschnitt betitelt „5. Funktionale Anforderungen“5.1 Mobile App (📱)
Abschnitt betitelt „5.1 Mobile App (📱)“- F-M-01 — NFC-Fahrzeug-Anmeldung. NTAG213 (180 Byte) oder NTAG215 (504 Byte) mit einem NDEF-Record
application/vnd.werkszeit.vehicleund Payload{ "vehicleId": "...", "tenantId": "...", "nonce": "..." }. Lese-Distanz verpflichtend <4 cm (Default bei NTAG-Reichweite ~3 cm), damit unbeabsichtigtes Auslesen ausgeschlossen ist. Falls der Tag unbekannt ist → Onboarding-Flow (EC-01). - F-M-02 — Fahrzeug-Kontext-Banner. Nach erfolgreicher NFC-Anmeldung zeigt eine persistente Banner-Zeile
Fahrzeug M-SG 247 · 412 Positionen · zuletzt gesynct 07:02 Uhr. Der Kontext gilt bis 18:00 Uhr oder bis erneutes NFC-Tap. - F-M-03 — GS1-128-Scan. Code-128 mit Application Identifiers:
(01)GTIN-14,(10)Charge,(17)Verfallsdatum YYMMDD,(21)Seriennummer,(30)Menge. Parser laut GS1 General Specifications v24 (Release 2024, 2026 gültig). Fallback: EAN-13, QR, GS1 DataMatrix. Hardware: Kamera (mobile_scanner) + Bluetooth-HID (Zebra RS5100, Honeywell Voyager 1452g). - F-M-04 — Stapel-Scan-Modus. Kamera bleibt offen, Vibrations-Feedback pro Scan, Anzeige der letzten 5 Scans als Liste. Abschluss per großem Orange-Button „Fertig · 14 Positionen“.
- F-M-05 — Manuelle Entnahme. Wenn kein Code vorhanden → Stichwort-Suche in Tenant-Artikel-Stammdaten, Menge, optional Foto. Foto wird als S3-Multipart-Upload über presigned URL hochgeladen (Drift speichert nur Thumbnail + Referenz, DOD §2.5).
- F-M-06 — Rückbuchung. Gleicher Flow, aber mit Toggle „Rückbuchung“; Grund-Feld (Pflicht bei GoBD: „Bauherr hat nicht eingebaut“, „Beschädigt“, „Falsch gegriffen“).
- F-M-07 — Mindestbestand-Warnung + Bestellvorschlag-Karte. Bei Unterschreitung der
reorderThresholdim Stammdaten-Profil: Push-Notification + Card mit Artikel, Vorschlagsmenge (Differenz zutargetStock), bevorzugtem Händler (aus Rahmenvertrag). Ein-Tap-Bestätigung legt Position in denorderCartdes Fahrzeugs. - F-M-08 — OCI-Punchout auslösen. Button „Zu Hagebau / Würth / Sonepar bestellen“ öffnet ein
flutter_inappwebviewauf die OCI-URL des konfigurierten Händlers; der Händler übernimmt Authentifizierung mit vorab generiertem HMAC-Session-Token. - F-M-09 — OCI-Hook-Empfang. Rückkehr vom Händler-Portal per
HOOK_URL-POST → Deep-Link zurück in die App → Warenkorb-Darstellung ausNEW_ITEM-*-Feldern → Freigabe-Workflow. - F-M-10 — Lieferung an Baustelle. Check-Option „Lieferadresse = aktuelle Baustelle (GPS-Match)“ statt Firmenzentrale.
- F-M-11 — Inventur-Zählung mobil. Stichtag vom Admin eröffnet, App zeigt alle im Fahrzeug erwarteten Artikel als Zählliste, Scan springt zur Position, Ist-Menge eintragen, Foto optional, Speichern → Differenz-Flag.
- F-M-12 — Offline-Verhalten. Voll-offline (DOR §1.2.9): Stapel-Scans und Entnahmen gehen in die Outbox mit UUID-v7-Idempotency-Key. OCI-Punchout ist zwangsläufig online-only (Browser gegen Händler), die UI zeigt „Offline — Bestellvorschlag wird gespeichert, Punchout bei nächster Verbindung“.
5.2 Web-App (🌐)
Abschnitt betitelt „5.2 Web-App (🌐)“- F-W-01 — Bestandsübersicht pro Fahrzeug mit Soll/Ist/Delta-Heatmap.
- F-W-02 — Bestellvorschlags-Kanban (Vorschlag → Freigabe durch Manager → Punchout → Eingegangen → Gebucht).
- F-W-03 — Konflikt-Ticket-Liste (aus F-X-01, siehe unten).
- F-W-04 — OCI-Händler-Konfiguration (URL, Hook-Secret, Credentials, Sandbox-Umschalter, Test-Punchout).
- F-W-05 — Stammdaten-Import BMEcat 2005 und GAEB XML v3.3 für Artikel-Stamm.
- F-W-06 — Inventur-Lauf-Verwaltung: Stichtag setzen, Snapshot-Freeze, Fortschritts-Matrix (Fahrzeug × Artikel), Differenz-Bericht mit einem Klick „Korrekturbuchung erzeugen“.
- F-W-07 — GoBD-Export: CSV oder IDEA-Schnittstelle (ZIP mit
index.xml,Bestandsbewegungen.csv), §13b-UStG-Liste (Material-Beistellungen an Sub-Unternehmer). - F-W-08 — Bulk-Aktionen (Korrekturbuchung, Umlagerung Fahrzeug→Fahrzeug, Ausbuchung Schwund mit Grund-Pflichtfeld).
5.3 Cross-Plattform (🔄)
Abschnitt betitelt „5.3 Cross-Plattform (🔄)“- F-X-01 — Konflikt-Auflösung bei Negativ-Bestand. Algorithmus: Semantischer Merge, keine Last-Write-Wins.
- Jede Entnahme ist eine Delta-Operation (
-5 Stk Artikel X), nicht ein Set-Operation. Deltas sind additiv-kommutativ (PN-Counter-CRDT-artig). - Server wendet Deltas in Serialisierungs-Reihenfolge des ersten Eintreffens an.
- Wenn
bestandNachDelta < 0: Das später eintreffende Delta wird aufbestand_verfügbargedeckelt (z. B. 3 statt 8 Stk), der Rest (5 Stk) wird alsLieferverzugTicketerzeugt. Auto-Triggerrestbestand == 0 → Sofort-Lieferauftraggreift auf den bevorzugten Händler-Rahmenvertrag zu. - Der zu kurz gekommene Monteur bekommt einen Push mit Alternativen: (a) anderes Fahrzeug im 5-km-Radius (Geo-Match aus Dienstplan), (b) Schnell-Nachbestellung mit Liefer-ETA aus OCI-Scraping.
- Jede Entnahme ist eine Delta-Operation (
- F-X-02 — Audit-Log-Eintrag pro Bestandsmutation:
material.stock.mutated,material.order.submitted,material.inventory.corrected.
5.4 Admin-Konfiguration
Abschnitt betitelt „5.4 Admin-Konfiguration“- F-A-01 — NFC-Tag-Onboarding: Admin scannt neuen leeren Tag, weist Fahrzeug zu, App schreibt NDEF-Record (schreibgeschützt setzen per CC-Lock-Byte-0x0F). Tag-UID in
vehicle_nfc_tag-Tabelle. - F-A-02 — Soll-Bestands-Profile pro Fahrzeug-Klasse (SHK-Vollausstattung, Elektro-Kleinbus, Notdienst-Wagen).
- F-A-03 — Auto-Replenishment-Regeln (z. B. Montagmorgens 04:00 Uhr automatisch Cart befüllen, Freigabe-Schwelle 500 €).
- F-A-04 — OCI-Händler-Katalog: vordefinierte Konfigurationen für GC-Gruppe, Würth, Sonepar, Hagebau, Conrad Business.
5.5 Roadmap-Schichtung
Abschnitt betitelt „5.5 Roadmap-Schichtung“| Anforderung-ID | V2-MVP (Monat 15–18) | V2 (Monat 19–22) | V2.5 (Monat 23+) |
|---|---|---|---|
| F-M-01, -02, -03, -04, -05, -06 | ✅ | ||
| F-M-07, -08, -09 | ✅ | ||
| F-M-10, -11 | ✅ | ||
| F-M-12 | ✅ | ||
| F-W-01, -02, -04 | ✅ | ||
| F-W-03, -05, -06 | ✅ | ||
| F-W-07, -08 | ✅ | ||
| F-X-01, -02 | ✅ | ||
| F-A-01, -02, -04 | ✅ | ||
| F-A-03 | ✅ |
6. Mockups & Flows
Abschnitt betitelt „6. Mockups & Flows“HTML-Hero-Mockup: 06-mobiles-lager-material.html — Mobile + Web Side-by-Side mit Persona Mehmet Yılmaz (Monteur) und Thomas Schmidt (Bauleiter), Beispiel-Artikel „Schraube DIN 7991 M8x40 Edelstahl A2“, Bestand 47, Mindestbestand 20.
6.1 Mobile — Fahrzeug-Anmeldung + Bestands-Home
Abschnitt betitelt „6.1 Mobile — Fahrzeug-Anmeldung + Bestands-Home“┌────────────────────────────────┐│ 🛡 GoBD · §13b · DSGVO │ ← Compliance-Banner├────────────────────────────────┤│ ← Lager ⚙ │├────────────────────────────────┤│ 📡 Tag erkannt ││ Fahrzeug M-SG 247 ││ Kastenwagen SHK · Mehmet Y. ││ zuletzt gesynct 07:02 ││ ││ Bestand: 412 Positionen ││ ┌────────────────────────────┐ ││ │ 🔴 3 unter Mindestbestand │ ││ │ → Bestellvorschlag (3) │ ││ └────────────────────────────┘ ││ ││ [📷 SCAN] [+ Manuell] [Inv.] ││ ││ Zuletzt entnommen: ││ 06:58 Winkel DN 15 −2 ││ 17.04. Kupferrohr 18mm −3 m ││ 17.04. Dichtung DN 20 −5 │├────────────────────────────────┤│ [Zeit] [Plan] [Doku] [Lager] │└────────────────────────────────┘6.2 Mobile — Scan-Flow (Stapel-Modus)
Abschnitt betitelt „6.2 Mobile — Scan-Flow (Stapel-Modus)“┌────────────────────────────────┐│ ← Scan · Fahrzeug M-SG 247 │├────────────────────────────────┤│ ││ ┌──────────────────────┐ ││ │ ║║ ║║║║ ║║ ║║║ ║║║ │ │ ← Live-Kamera│ │ (GS1-128 Bereich) │ ││ └──────────────────────┘ ││ ││ Letzte Scans: ││ ✓ Schraube DIN 7991 M8x40 −10 ││ (01)04046962340127 ││ (10)CHG-2026-04-A ││ ✓ Dichtung DN 20 −4 ││ ✓ Kupfer-T-Stück 22mm −2 ││ ││ 🔴 Pflicht-Feld: Kostenstelle ││ Projekt Lehrer Allee 7 ▼ ││ ││ [ FERTIG · 3 Positionen ] │└────────────────────────────────┘6.3 Mobile — Mindestbestand-Warnung + Bestellvorschlag
Abschnitt betitelt „6.3 Mobile — Mindestbestand-Warnung + Bestellvorschlag“┌────────────────────────────────┐│ ← Bestellvorschlag │├────────────────────────────────┤│ ⚠ Mindestbestand unterschritten││ ││ Schraube DIN 7991 M8x40 ││ Edelstahl A2 ││ ┌────────────────────────────┐ ││ │ Ist: 37 Stk │ ││ │ Min: 20 Stk │ ││ │ Soll: 100 Stk │ ││ │ Nachbestellen: 63 Stk │ ││ └────────────────────────────┘ ││ ││ Händler: Würth (Rahmenvertrag) ││ Preis: ≈ 14,70 € ││ ETA: morgen 10:00 ││ ││ Lieferadr.: ⦿ Baustelle Lehrer ││ ○ Werkstatt M-10 ││ ││ [ In Cart ] [ Jetzt bestellen]│└────────────────────────────────┘6.4 Mobile — OCI-Punchout Übergang
Abschnitt betitelt „6.4 Mobile — OCI-Punchout Übergang“┌────────────────────────────────┐│ ← Hagebau-Portal (OCI 4.0) │├────────────────────────────────┤│ 🔒 hagebau.de/punchout/... ││ ┌────────────────────────────┐ ││ │ │ ││ │ [ Hagebau-Katalog ] │ ││ │ │ ││ │ Sie wurden von │ ││ │ Werkszeit (mustermann) │ ││ │ eingebucht — Cart wird │ ││ │ automatisch übergeben │ ││ │ │ ││ │ [ ▶ Cart übertragen ] │ ││ └────────────────────────────┘ ││ ││ Session-Timeout in 09:42 │└────────────────────────────────┘6.5 Web — Bestandsübersicht + Konflikt-Panel
Abschnitt betitelt „6.5 Web — Bestandsübersicht + Konflikt-Panel“┌──────────────────────────────────────────────────────────────────────────────┐│ Werkszeit · shk-gebruder-schmidt Thomas S. · Bauleitung [Profil] │├──────────────────────────────────────────────────────────────────────────────┤│ Sidebar │ Lager · Fahrzeug M-SG 247 Mo 19.04.2026 · gesynct 09:42 ││ │ ┌────────────────────────────────────────────────────────────┐ ││ ▶ Lager │ │ KPI Positionen 412 · Unter Min. 3 · Wert 18.472 € │ ││ Bestell.│ │ Konflikte heute 1 · Offene Tickets 1 · Ø Alter 2 h │ ││ Inventur│ └────────────────────────────────────────────────────────────┘ ││ Händler │ ││ │ Artikel │ Soll │ Ist │ Δ │ Status ││ │ Schraube DIN 7991 M8x40 A2 │ 100 │ 47 │-53 │ 🟢 OK ││ │ Winkel DN 15 │ 40 │ 0 │-40 │ 🔴 Negativ! ││ │ Kupferrohr 18 mm (m) │ 30 │ 24 │ -6 │ 🟡 Knapp ││ │ Dichtung DN 20 │ 60 │ 55 │ -5 │ 🟢 OK ││ │ … ││ │ ││ │ ┌ Konflikt-Ticket #LGR-2026-0412 ──────────────────────────┐ ││ │ │ Artikel: Winkel DN 15 │ ││ │ │ Monteur A: Mehmet Y. hat 5 Stk entnommen (offline 06:58) │ ││ │ │ Monteur B: Andreas W. hat 8 Stk entnommen (offline 07:11)│ ││ │ │ Bestand vor Konflikt: 10 Stk │ ││ │ │ Auto-Resolution: A bekommt 5, B bekommt 5, B fehlen 3 │ ││ │ │ Auto-Aktion: Lieferauftrag Würth 20 Stk ausgelöst │ ││ │ │ │ ││ │ │ [ Genehmigen ] [ Manuell anpassen ] [ Alternativen 📍 ]│ ││ │ └────────────────────────────────────────────────────────── ┘ │└──────────────────────────────────────────────────────────────────────────────┘6.6 Web — Inventur-Differenz-Bericht
Abschnitt betitelt „6.6 Web — Inventur-Differenz-Bericht“┌──────────────────────────────────────────────────────────────────────────────┐│ Inventur · Stichtag 31.03.2026 · Schluss: 02.04.2026 12:00 · 87% gezählt │├──────────────────────────────────────────────────────────────────────────────┤│ Fahrzeug │ Pos.│ Gezählt │ Differenz │ Wert-Diff │ Status ││ M-SG 247 │ 412 │ 412 │ −3 Stk │ −47,20 € │ ✓ Abgeschlossen ││ M-SG 312 │ 389 │ 389 │ +12 Stk │ +132,50 € │ ✓ Abgeschlossen ││ M-SG 445 │ 401 │ 312 │ — │ — │ ⚠ 78% · offen ││ ││ Differenzen (−47,20 €): Haupttreiber: Kupferrohr 18 mm −4 m (37,60 €) ││ Schraube DIN 7991 −8 Stk (2,40 €) ││ ││ [ Korrekturbuchungen erzeugen (GoBD-konform) ] [ Detail-PDF ] │└──────────────────────────────────────────────────────────────────────────────┘Symbol-Konvention nach TEMPLATE.md §6.
7. Datenmodell-Skizze
Abschnitt betitelt „7. Datenmodell-Skizze“import { pgTable, uuid, text, numeric, integer, timestamp, boolean, bytea, jsonb, index, unique } from 'drizzle-orm/pg-core';
export const vehiclesTable = pgTable('vehicles', { id: uuid('id').primaryKey().defaultRandom(), // UUID v7 tenantId: uuid('tenant_id').notNull(), plate: text('plate').notNull(), // "M-SG 247" profileId: uuid('profile_id').notNull(), // Fahrzeug-Soll-Profil active: boolean('active').notNull().default(true), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull(),}, (t) => ({ tenantIdx: index('vehicles_tenant_idx').on(t.tenantId), tenantPlate: unique('vehicles_tenant_plate_unique').on(t.tenantId, t.plate),}));
export const vehicleNfcTagsTable = pgTable('vehicle_nfc_tags', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), vehicleId: uuid('vehicle_id').notNull().references(() => vehiclesTable.id), nfcUid: text('nfc_uid').notNull(), // ISO14443-A UID chipType: text('chip_type').notNull(), // 'NTAG213' | 'NTAG215' nonce: bytea('nonce').notNull(), // für HMAC-Anti-Replay locked: boolean('locked').notNull().default(false), // CC-Lock-Byte gesetzt lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),}, (t) => ({ tenantUid: unique('nfc_tenant_uid_unique').on(t.tenantId, t.nfcUid),}));
export const articlesTable = pgTable('articles', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), gtin14: text('gtin14'), // AI(01), null falls kein Barcode sku: text('sku').notNull(), // interner Schlüssel name: text('name').notNull(), // "Schraube DIN 7991 M8x40 Edelstahl A2" uom: text('uom').notNull(), // 'PCE' | 'M' | 'KG' etimClass: text('etim_class'), // ETIM-9.0 vatRate: numeric('vat_rate', { precision: 5, scale: 2 }).notNull(), listPrice: numeric('list_price', { precision: 12, scale: 4 }).notNull(), hazardClass: text('hazard_class'), // ADR-Gefahrgut, optional createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),}, (t) => ({ tenantGtin: index('articles_tenant_gtin_idx').on(t.tenantId, t.gtin14), tenantSku: unique('articles_tenant_sku_unique').on(t.tenantId, t.sku),}));
export const stockLevelsTable = pgTable('stock_levels', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), vehicleId: uuid('vehicle_id').notNull().references(() => vehiclesTable.id), articleId: uuid('article_id').notNull().references(() => articlesTable.id), quantity: numeric('quantity', { precision: 14, scale: 4 }).notNull().default('0'), minStock: numeric('min_stock', { precision: 14, scale: 4 }).notNull(), targetStock: numeric('target_stock', { precision: 14, scale: 4 }).notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),}, (t) => ({ tenantVehArt: unique('stock_tenant_veh_art_unique').on(t.tenantId, t.vehicleId, t.articleId),}));
// Append-only Bestandsbewegungen — GoBD-relevant, Hash-Chainexport const stockMovementsTable = pgTable('stock_movements', { id: uuid('id').primaryKey().defaultRandom(), // UUID v7 tenantId: uuid('tenant_id').notNull(), vehicleId: uuid('vehicle_id').notNull(), articleId: uuid('article_id').notNull(), movementType: text('movement_type').notNull(), // 'ISSUE' | 'RETURN' | 'RECEIPT' | 'CORRECTION' | 'INVENTORY_SET' | 'TRANSFER_IN' | 'TRANSFER_OUT' deltaQty: numeric('delta_qty', { precision: 14, scale: 4 }).notNull(), // Vorzeichen-behaftet reason: text('reason'), // Pflicht bei CORRECTION / RETURN batchNo: text('batch_no'), // GS1 AI(10) expiryDate: text('expiry_date'), // GS1 AI(17) YYMMDD serialNo: text('serial_no'), // GS1 AI(21) projectId: uuid('project_id'), // Kostenstelle (Cross-Link §4.5) sourceOp: text('source_op').notNull(), // 'SCAN' | 'MANUAL' | 'OCI_RECEIPT' | 'INVENTORY' | 'CONFLICT_RESOLVE' idempotencyKey: uuid('idempotency_key').notNull(), // UUID v7 aus Client-Outbox clientTs: timestamp('client_ts', { withTimezone: true }).notNull(), // Zeitstempel am Gerät serverTs: timestamp('server_ts', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull(), // GoBD Hash-Chain hashPrev: bytea('hash_prev'), hashSelf: bytea('hash_self').notNull(),}, (t) => ({ tenantVehIdx: index('mov_tenant_veh_idx').on(t.tenantId, t.vehicleId, t.serverTs.desc()), tenantArtIdx: index('mov_tenant_art_idx').on(t.tenantId, t.articleId, t.serverTs.desc()), tenantIdemp: unique('mov_tenant_idemp_unique').on(t.tenantId, t.idempotencyKey), tenantProj: index('mov_tenant_proj_idx').on(t.tenantId, t.projectId, t.serverTs.desc()),}));
export const ociConfigsTable = pgTable('oci_configs', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), vendor: text('vendor').notNull(), // 'HAGEBAU' | 'WUERTH' | 'SONEPAR' | 'GC_GRUPPE' | 'CONRAD' punchoutUrl: text('punchout_url').notNull(), // HTTPS hookSecret: bytea('hook_secret').notNull(), // HMAC-Secret für Hook-Signatur username: text('username').notNull(), passwordEnc: bytea('password_enc').notNull(), // KMS-verschlüsselt sandbox: boolean('sandbox').notNull().default(false), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),});
export const orderCartsTable = pgTable('order_carts', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), vehicleId: uuid('vehicle_id').notNull(), projectId: uuid('project_id'), status: text('status').notNull(), // 'DRAFT' | 'PUNCHOUT_OPEN' | 'SUBMITTED' | 'CONFIRMED' | 'DELIVERED' | 'BOOKED' | 'CANCELLED' vendor: text('vendor'), totalNet: numeric('total_net', { precision: 14, scale: 2 }), hookPayload: jsonb('hook_payload'), // OCI-Rückgabe-Daten createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull(),}, (t) => ({ tenantVehIdx: index('carts_tenant_veh_idx').on(t.tenantId, t.vehicleId, t.createdAt.desc()),}));
export const orderCartLinesTable = pgTable('order_cart_lines', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), cartId: uuid('cart_id').notNull().references(() => orderCartsTable.id), articleId: uuid('article_id'), // null bei neuen Händler-Artikeln vendorSku: text('vendor_sku'), description: text('description').notNull(), quantity: numeric('quantity', { precision: 14, scale: 4 }).notNull(), unitPrice: numeric('unit_price', { precision: 14, scale: 4 }).notNull(),});
export const inventoryRunsTable = pgTable('inventory_runs', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), cutoffDate: text('cutoff_date').notNull(), // ISO-Datum snapshotAt: timestamp('snapshot_at', { withTimezone: true }).notNull(), status: text('status').notNull(), // 'OPEN' | 'IN_PROGRESS' | 'CLOSED' snapshotJson:jsonb('snapshot_json').notNull(), // Bestände aller Fahrzeuge zum Stichtag createdBy: uuid('created_by').notNull(),});
export const conflictTicketsTable = pgTable('conflict_tickets', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), vehicleId: uuid('vehicle_id').notNull(), articleId: uuid('article_id').notNull(), movementAId: uuid('movement_a_id').notNull(), movementBId: uuid('movement_b_id').notNull(), deficitQty: numeric('deficit_qty', { precision: 14, scale: 4 }).notNull(), autoActionTaken: text('auto_action_taken').notNull(), // 'ORDER_TRIGGERED' | 'ALTERNATIVE_SUGGESTED' | 'NONE' status: text('status').notNull(), // 'OPEN' | 'RESOLVED' | 'REJECTED' resolvedAt: timestamp('resolved_at', { withTimezone: true }),});RLS-Policy-Sketch (exemplarisch für stock_movements, analog für alle anderen):
ALTER TABLE stock_movements ENABLE ROW LEVEL SECURITY;
CREATE POLICY stock_movements_tenant_isolation ON stock_movements USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY stock_movements_scope ON stock_movements FOR SELECT USING ( tenant_id = current_setting('app.tenant_id')::uuid AND ( current_setting('app.scope') = 'all' OR (current_setting('app.scope') = 'team' AND vehicle_id IN (SELECT vehicle_id FROM team_vehicle_assignments WHERE team_id = current_setting('app.team_id')::uuid)) OR (current_setting('app.scope') = 'own' AND created_by = current_setting('app.user_id')::uuid) ) );
-- Append-only erzwingen:CREATE POLICY stock_movements_no_update ON stock_movements FOR UPDATE USING (false);CREATE POLICY stock_movements_no_delete ON stock_movements FOR DELETE USING (false);ER-Bezug. users, tenants, projects, teams aus Kern-Schema; audit_log aus §11-Feinkonzept wird pro Bestandsmutation beschrieben.
8. API-Endpunkte
Abschnitt betitelt „8. API-Endpunkte“| Methode | Pfad | Auth-Scope | Rate-Limit | Idempotenz | Beschreibung |
|---|---|---|---|---|---|
POST |
/v1/material/vehicles/{id}/checkin |
material:write |
Standard | Idempotency-Key Pflicht | NFC-Anmeldung (Nonce-Prüfung, Last-Seen-Update) |
GET |
/v1/material/vehicles/{id}/stock |
material:read:team |
Standard | — | Bestandsübersicht |
POST |
/v1/material/movements |
material:write |
Burst | Idempotency-Key Pflicht | Entnahme/Rückbuchung (Client-Outbox-Push) |
POST |
/v1/material/movements:bulk |
material:write |
Burst | Idempotency-Key Pflicht | Stapel-Scan |
GET |
/v1/material/articles |
material:read:team |
Standard | — | Artikel-Suche (FTS) |
POST |
/v1/material/carts |
material:write |
Standard | Idempotency-Key Pflicht | Cart aus Bestellvorschlag |
POST |
/v1/material/carts/{id}/punchout |
material:write |
Privileged | Idempotency-Key Pflicht | OCI-Session eröffnen → HTML-Form mit HOOK_URL zurück |
POST |
/v1/material/oci/hook |
material:integration |
Burst | HMAC-Signatur | OCI-Cart-Return (Public-Endpoint mit HMAC) |
POST |
/v1/material/carts/{id}/submit |
material:approve |
Standard | Idempotency-Key Pflicht | Bestellung final rausschicken |
POST |
/v1/material/inventory/runs |
material:approve |
Standard | Idempotency-Key Pflicht | Inventur eröffnen |
POST |
/v1/material/inventory/runs/{id}/count |
material:write |
Burst | Idempotency-Key Pflicht | Zählung pro Artikel |
POST |
/v1/material/inventory/runs/{id}/close |
material:approve |
Privileged | Idempotency-Key Pflicht | Differenzbuchungen auslösen |
GET |
/v1/material/conflicts |
material:read:team |
Standard | — | Konflikt-Tickets |
POST |
/v1/material/conflicts/{id}/resolve |
material:approve |
Standard | Idempotency-Key Pflicht | Manuelle Resolution |
GET |
/v1/material/export/gobd |
material:export |
Privileged | — | IDEA-ZIP mit index.xml |
OpenAPI-Schema-Skizze:
paths: /v1/material/movements: post: operationId: createMovement x-werkszeit-scope: material:write x-werkszeit-rate-limit: burst parameters: - in: header name: Idempotency-Key required: true schema: { type: string, format: uuid } requestBody: required: true content: application/json: schema: type: object required: [vehicleId, articleId, deltaQty, movementType, clientTs, sourceOp] properties: vehicleId: { type: string, format: uuid } articleId: { type: string, format: uuid } deltaQty: { type: string, pattern: '^-?\\d+(\\.\\d{1,4})?$' } movementType: { type: string, enum: [ISSUE, RETURN, CORRECTION, INVENTORY_SET] } reason: { type: string, maxLength: 500 } batchNo: { type: string, maxLength: 20 } expiryDate: { type: string, pattern: '^[0-9]{6}$' } projectId: { type: string, format: uuid, nullable: true } clientTs: { type: string, format: date-time } sourceOp: { type: string, enum: [SCAN, MANUAL, INVENTORY] } responses: '201': { description: Buchung erzeugt } '409': { description: Idempotency-Konflikt } '422': { description: Fachlicher Konflikt (Bestand würde <0) — enthält conflictTicketId }Webhook-Events: material.stock.mutated, material.stock.low, material.order.cart_created, material.order.punchout_returned, material.order.submitted, material.inventory.opened, material.inventory.closed, material.conflict.opened, material.conflict.resolved.
9. Offline-Profil
Abschnitt betitelt „9. Offline-Profil“| Profil | Begründung | Mechanik |
|---|---|---|
| Voll-offline (Entnahme, Rückbuchung, Stapel-Scan, Inventur-Zählung) | Monteur im Heizungskeller ohne LTE ist der Normalfall — DOR §1.2.9 | Outbox in Drift mit UUID-v7-Idempotency-Key, Delta-Semantik (nicht Set-Semantik). Retry bei connectivity == online. Lokal wird optimistisch gebucht, aber mit Flag syncState=PENDING. |
| Best-effort-offline (Bestellvorschlag erzeugen, Cart-Zeile hinzufügen) | Vorschlag kann lokal erzeugt werden, Vendor-Match braucht Server | Lokale Speicherung, Vendor-Auflösung beim Sync |
| Online-only (OCI-Punchout, Cart-Submit, Inventur-Close, GoBD-Export) | Punchout braucht aktive Händler-Session; Submit und Close sind GoBD-final | UI-Block mit klarem Hinweis, Retry when online |
Konflikt-Strategie. Siehe F-X-01: Delta-Semantik auf Server, automatischer Lieferauftrag bei Negativ-Resolution, niemals Last-Write-Wins. Append-only mit versionierter Korrektur ist der einzige Weg, der GoBD standhält. Hintergrund in LESSONS-LEARNED §4.
Foto-/Datei-Sync. Fotos (Entnahme-Beleg, Inventur-Beleg) gehen als S3-Multipart über presigned URL; Drift speichert thumbnailBase64 (max 60 KB) + S3-Key.
10. Compliance-Mapping
Abschnitt betitelt „10. Compliance-Mapping“10.1 GoBD
Abschnitt betitelt „10.1 GoBD“- Append-only-Tabelle:
stock_movementsmithash_prev+hash_self(SHA-256 überid || tenantId || vehicleId || articleId || movementType || deltaQty || reason || projectId || idempotencyKey || clientTs || serverTs || createdBy || hash_prev). Update/Delete-Policies auf RLS-Ebene verboten. - Aufbewahrung: Foto-Belege der Entnahme in S3 mit Object Lock (Compliance-Mode, 10 Jahre — §147 AO).
- Zeitnah: Buchung entsteht spätestens beim nächsten Sync;
serverTs − clientTs > 30 Tagewird in einen Audit-Warn-Report gehoben. - Nachvollziehbar: Jede Mutation erzeugt Audit-Log-Eintrag
material.stock.mutatedmit Ziel-ID, Akteur, Delta, Quelle (Scan/Manual/OCI/Inventur). - Unveränderbar: Korrekturen laufen immer als neue
CORRECTION-Buchung mit Pflicht-reason, nie als Update der Original-Zeile. - Prüfbar: GoBD-Export als ZIP mit
index.xml(GDPdU-Format, IDEA-kompatibel). - Verfahrensdokumentation: Textbaustein-Referenz
gobd-material-verfahrensdoku.md(Stichworte: „Warenbewegung als Beleg“, „Fahrzeug als Lagerort“, „Chargen-Rückverfolgung“).
10.2 ArbZG
Abschnitt betitelt „10.2 ArbZG“— nicht zutreffend, weil dieses Modul keine Arbeitszeit-Events erzeugt. Cross-Modul: Entnahmen liegen zeitlich innerhalb einer Stempelung, beeinflussen aber ArbZG-Berechnung nicht.
10.3 VOB/B
Abschnitt betitelt „10.3 VOB/B“— nicht zutreffend als Haupt-Compliance-Pfad. Material-Zuordnung zu Projekten fließt in die VOB-Abrechnung §4.5, dort greifen §14 VOB/B-Anforderungen.
10.4 DSGVO
Abschnitt betitelt „10.4 DSGVO“- Art. 5 Datenminimierung: Es werden keine personenbezogenen Standortdaten dauerhaft gespeichert. Nur die Zuordnung
createdBy = user_idpro Buchung ist personenbezogen (zulässig nach Art. 6 Abs. 1 b — Vertragserfüllung). - Art. 6 Rechtsgrundlage: Vertrag (Arbeitsvertrag, Betriebsmittel-Nutzung).
- Art. 17 Löschung:
createdBywird bei Lösch-Antrag eines Ex-Mitarbeiters aufanonymized-user-<tenant>maskiert (GoBD-Konflikt-Vermeidung: Beleg bleibt, Person wird maskiert). Fotos werden geprüft, ob sie Personen enthalten — falls ja, werden sie bei Löschantrag ausgewertet und ggf. unkenntlich gemacht oder gelöscht (Pflichtfeld-Prozess). - Art. 20 Datenexport: Der Ex-Mitarbeiter erhält in seinem DSGVO-JSON-Export die Liste aller von ihm getätigten Buchungen (nur seine
createdBy-Zeilen). - Art. 30 Verarbeitungsverzeichnis: Eintrag „Materialverwaltung Service-Fahrzeuge“ — Zweck: Warenwirtschaft; Empfänger: Handelspartner via OCI; Löschfrist: 10 Jahre (§147 AO).
- DSFA: Nicht Pflicht — keine systematische Überwachung, keine Hochrisiko-Verarbeitung.
10.5 BFSG / WCAG 2.2 AA
Abschnitt betitelt „10.5 BFSG / WCAG 2.2 AA“— nicht zutreffend, weil dieses Modul kein ESS-Flow ist (Mitarbeiter-Selbstbedienung im Sinne des BFSG adressiert Lohnzettel, Stammdaten, Urlaubsantrag). Die App-Screens erfüllen trotzdem WCAG 2.2 AA aus Konsistenz-Gründen (Fokus, Kontrast, Labels) und werden über axe-core (Web-Pendant) getestet.
10.6 BetrVG §87(1)6
Abschnitt betitelt „10.6 BetrVG §87(1)6“— nicht zutreffend. Das Modul erzeugt keine Leistungs- oder Verhaltenskontrolle. Wir tracken keine Entnahmequoten pro Mitarbeiter, kein Geofencing, keine Bewegungsprofile. Falls ein Kunde „Top-Entnahmen pro Mitarbeiter“-Reports fordert, wandert das hinter einen Betriebsrat-Freischaltungs-Workflow.
10.7 Weitere Spezial-Compliance
Abschnitt betitelt „10.7 Weitere Spezial-Compliance“- §13b UStG — Material-Beistellung: Export-Liste pro Projekt und Sub-Unternehmer mit Flag „Material auf Projekt“, damit der Steuerberater prüfen kann, ob die §13b-Reverse-Charge-Abgrenzung greift.
- §147 AO — Aufbewahrung: siehe §10.1.
- KI-VO Art. 50 — Transparenz: Der Auto-Bestellvorschlag (F-M-07) ist eine Heuristik (Soll-Ist-Differenz × Sicherheitsfaktor), keine KI im Sinne der AI-Act-Definition. Falls V2.5 ein ML-Forecast-Modul hinzukommt, wird dort der Transparenz-Hinweis „Vorschlag — bitte prüfen“ zur Pflicht.
- ElektroG / ProdHaftG: Chargen- und Seriennummer-Erfassung via GS1 AI(10) und AI(21) ist optional, aber empfohlen — für Elektro- und Sanitär-Bauteile kundenseitig aktivierbar.
- ADR-Gefahrgut:
articles.hazardClasswird in der Fahrzeug-Gesamtansicht aggregiert; ab gewisser Schwelle (250 Punkte) muss der Fahrer die ADR-Kleinmengenregel-Bescheinigung dabei haben. V2.5-Feature.
11. Edge-Cases
Abschnitt betitelt „11. Edge-Cases“| # | Szenario | Erwartetes Verhalten |
|---|---|---|
| EC-01 | NFC-Tag unbekannt / neu | Onboarding-Flow: App zeigt „Tag nicht registriert — Fahrzeug auswählen“, Mitarbeiter wählt Fahrzeug aus Dropdown, Admin bekommt Approval-Push (bei Scope own Mitarbeiter nicht Self-Service), nach Admin-OK wird vehicle_nfc_tag-Zeile angelegt. |
| EC-02 | OCI-Punchout-Timeout (Händler antwortet nicht innerhalb 60 s) | Frontend zeigt Retry-Dialog, Cart bleibt im Status PUNCHOUT_OPEN, nach 3 Retries Fallback auf manuelle Bestellung mit Copy-Paste-Cart-Text. Audit-Eintrag material.oci.timeout. |
| EC-03 | Inventur-Differenz negativ (gezählt < erwartet) | Differenz wird als CORRECTION-Buchung mit reason="Inventurdifferenz Stichtag X" erfasst, Manager-Freigabe nötig, optional Foto-Beleg. |
| EC-04 | Multi-Tenant-Bleed-Versuch (User mit 2 Tenants wechselt Kontext, scannt Tag von Tenant B) | Server weist Check-in ab mit 404 (nicht 403 — Existenz nicht offenbaren). Kein Audit-Eintrag in Tenant B. |
| EC-05 | Sync-Konflikt: Offline-Doppelentnahme | Siehe F-X-01. A bekommt Soll, B bekommt Rest, Delta als conflict_ticket, Auto-Nachbestellung. |
| EC-06 | GS1-128 mit Pflicht-Verfallsdatum überschritten | App zeigt Warnung „Verfallsdatum 26.03.2026 überschritten — Entnahme für Kundenprojekt blockiert“, Admin kann Override-Scope mit Grund setzen. |
| EC-07 | Hardware-Scanner koppelt, aber keine Kamera-Permission | App schlägt Fallback auf Bluetooth-HID vor, Kamera-Einstellung im Settings-Screen verlinkt. |
| EC-08 | Großmenge-Export (Manager zieht 120.000 Bewegungen als CSV) | Asynchroner Job, E-Mail mit Download-Link, max. Gültigkeit 24 h, S3-pre-signed. |
| EC-09 | NFC-Tag wurde manipuliert (Nonce stimmt nicht) | HMAC-Check scheitert, Server gibt 409, Audit-Warn-Eintrag material.nfc.tampered, Admin-Eskalation. |
| EC-10 | Plan-Limit Starter erreicht (Material nicht inkludiert) | Modul-Gate module.material ist off, Backend-Routes antworten mit 404. Frontend zeigt Upgrade-Screen. |
| EC-11 | Rollen-Demotion während laufender Bulk-Inventur | Serverseitig wird pro Zähl-Request die aktuelle Rolle geprüft, abgebrochen mit 403 sobald Rechte verloren. |
| EC-12 | OCI-Hook mit falschem HMAC | 401, Audit-Eintrag material.oci.hook.unauthorized, Admin-Warnung. |
12. Akzeptanzkriterien (Gherkin)
Abschnitt betitelt „12. Akzeptanzkriterien (Gherkin)“Funktionalität: Mobiles Lager & Material-Management
Hintergrund: Angenommen ein Tenant "shk-gebruder-schmidt" mit aktivem Modul-Gate "module.material" Und ein Monteur "Mehmet" mit Rolle "Mitarbeiter" und Scope "own" Und ein Fahrzeug "M-SG 247" mit NFC-Tag "04A1F2..." und Soll-Profil "SHK-Vollausstattung" Und ein Artikel "Schraube DIN 7991 M8x40 Edelstahl A2" mit GTIN "04046962340127", Bestand 47, Mindestbestand 20
Szenario: Happy Path — NFC-Anmeldung, GS1-Scan, Entnahme Wenn Mehmet sein Gerät an das NFC-Siegel "M-SG 247" hält Und die App den NDEF-Record vollständig liest Dann wird die Fahrzeug-Anmeldung mit Nonce-Prüfung bestätigt Und die App zeigt "Fahrzeug M-SG 247 · 412 Positionen" Wenn Mehmet den Stapel-Scan öffnet und den GS1-128-Code "(01)04046962340127(10)CHG-2026-04-A(30)10" scannt Dann erscheint die Position "Schraube DIN 7991 M8x40 Edelstahl A2 · −10" mit Charge "CHG-2026-04-A" Wenn Mehmet das Projekt "BV Lehrer Allee 7" als Kostenstelle wählt und auf "Fertig" tippt Dann wird genau eine Bewegung vom Typ "ISSUE" mit delta_qty=-10 gespeichert Und der Bestand steht auf 37 Und ein Audit-Log-Eintrag "material.stock.mutated" entsteht Und die Bewegung ist hash-verkettet mit der vorigen Bewegung dieses Tenants
Szenario: Grenzfall — NFC-Tag unbekannt (Onboarding-Flow, EC-01) Angenommen ein NFC-Tag mit UID "04B2E3..." ist in keiner "vehicle_nfc_tags"-Zeile registriert Wenn Mehmet diesen Tag liest Dann zeigt die App "Tag nicht registriert — Fahrzeug auswählen" Und der Fahrzeug-Dropdown listet nur Fahrzeuge mit Scope "team" für Mehmet Wenn Mehmet "M-SG 312" wählt und auf "Registrieren" tippt Dann wird ein Approval-Push an einen Admin gesendet Und der Tag ist bis zur Admin-Freigabe im Status "PENDING" Und Mehmet erhält die Anmeldung erst nach Admin-Approve
Szenario: Grenzfall — OCI-Punchout-Timeout (EC-02) Angenommen eine OCI-Konfiguration für Vendor "HAGEBAU" mit punchout_url "https://punchout.hagebau.de/..." Und Mehmet hat einen Cart mit 3 Positionen erstellt Wenn Mehmet auf "Jetzt bestellen" tippt Und der Hagebau-Server innerhalb 60 Sekunden nicht antwortet Dann zeigt die App einen Retry-Dialog Und der Cart-Status bleibt "PUNCHOUT_OPEN" Wenn Mehmet 3 Retries erfolglos durchführt Dann bietet die App einen manuellen Fallback mit exportierbarem Cart-Text Und ein Audit-Log-Eintrag "material.oci.timeout" entsteht
Szenario: Grenzfall — Inventur-Differenz negativ (EC-03) Angenommen ein Inventur-Lauf zum Stichtag "31.03.2026" Und der Snapshot erwartet 47 Stk "Schraube DIN 7991 M8x40" Wenn Mehmet bei der Zählung 39 Stk erfasst Dann wird eine "CORRECTION"-Bewegung mit delta_qty=-8 und reason="Inventurdifferenz 2026-03-31" als Entwurf angelegt Und die Differenz erscheint rot im Differenz-Bericht Wenn der Manager auf "Korrekturbuchungen erzeugen" klickt Dann werden die Korrekturen mit Hash-Chain final Und die Audit-Log-Einträge "material.inventory.corrected" werden erzeugt
Szenario: Grenzfall — Offline-Doppelentnahme (F-X-01) Angenommen Artikel "Winkel DN 15", Bestand 10 Stk im Fahrzeug M-SG 247 Und Mehmet ist offline seit 06:30 Uhr Und Andreas ist offline seit 06:45 Uhr (beide gleiches Fahrzeug via Team-Scope) Wenn Mehmet lokal 5 Stk entnimmt (Idempotency-Key A, clientTs 06:58) Und Andreas lokal 8 Stk entnimmt (Idempotency-Key B, clientTs 07:11) Und beide um 08:00 Uhr syncen (Mehmet zuerst) Dann wird Mehmet's Delta "-5" sauber angewendet (Bestand 5) Und Andreas' Delta wird auf "-5" gedeckelt (Bestand 0) Und ein Conflict-Ticket entsteht mit deficit_qty=3 Und ein Auto-Lieferauftrag an den Rahmenvertrags-Händler wird ausgelöst Und Andreas erhält einen Push mit Alternativen (anderes Fahrzeug im 5-km-Radius + ETA) Und beide Bewegungen sind in stock_movements persistent (keine wird gelöscht)13. Test-Cases
Abschnitt betitelt „13. Test-Cases“13.1 Unit-Tests
Abschnitt betitelt „13.1 Unit-Tests“- U-01 — GS1-128-Parser: Happy, FNC1-Trennung, AI mit fester Länge, AI mit variabler Länge (01, 10, 17, 21, 30), Grenzfall „Mehrfach-AI im Code“.
- U-02 — Delta-Semantik: Property-based, 1000 zufällige Delta-Folgen über identischen Bestand müssen zum gleichen Endergebnis wie
sum(deltas)führen. - U-03 — Hash-Chain: Manipulation einer Zeile invalidiert alle nachfolgenden Hashes.
- U-04 — OCI-XML-Roundtrip: Mustang-Cart in, Werkszeit-Cart-Lines out, Feld-Vollständigkeit.
- U-05 — Rundung und Mengeneinheiten:
PCE,M(bis 3 Nachkommastellen),KG.
13.2 Widget- / Component-Tests
Abschnitt betitelt „13.2 Widget- / Component-Tests“- W-01 — Stempel-Button (hier: „FERTIG · N Positionen“) disabled bei
len(scans) == 0. - W-02 — Golden-Test Mobile-Frame mit Compliance-Banner,
de-DE-Locale, Dark-Mode. - W-03 — Foto-Thumbnail-Upload zeigt Progress-Bar und fällt bei Netzverlust sauber in Queue.
13.3 Integrations-Tests
Abschnitt betitelt „13.3 Integrations-Tests“- I-01 — Multi-Tenant-Isolation (Pflicht): Tenant A darf keinen Cart, keine Movement, keinen Tag von Tenant B lesen/schreiben.
- I-02 — RLS
own: Mitarbeiter sieht nur eigene Entnahmen; Manager sieht team-weit. - I-03 — Outbox-Idempotenz: doppelter Request mit gleicher Idempotency-Key → 409 + keine Doppelbuchung.
- I-04 — OCI-Hook-HMAC-Verification: korrekter HMAC → 200; gefälschter HMAC → 401 + Audit-Eintrag.
13.4 E2E-Tests (Patrol / Playwright)
Abschnitt betitelt „13.4 E2E-Tests (Patrol / Playwright)“- E-01 — Happy Path aus §12 auf iOS + Android + Chromium/Firefox/WebKit.
- E-02 — Offline-Scan → Sync mit simuliertem Netzausfall (Patrol
network off → on+ 3 Buchungen in Outbox). - E-03 — OCI-Punchout gegen Sandbox-Mock-Server.
- E-04 — Inventur-Lauf End-to-End: Eröffnung → Zählung → Schluss → Korrektur-Hash-Chain.
- E-05 — Konflikt-Szenario: zwei parallele Offline-Geräte via Patrol-Multi-Device.
- E-06 — Accessibility: axe-core für Web-Dashboard ohne Critical/Serious, Flutter-Semantics-Test für Scan-Screen.
13.5 Compliance-Tests
Abschnitt betitelt „13.5 Compliance-Tests“- C-01 — Hash-Chain-Integrität: Mutation erkannt, Break-Index korrekt.
- C-02 — GoBD-IDEA-Export: ZIP entspricht GDPdU-Schema, von
idea-validateakzeptiert. - C-03 — GS1-Muster-Barcodes aus GS1 Germany Referenz-Set — 100 % korrekt geparst.
- C-04 — OCI 4.0 BMEcat-Return aus Hagebau-Sandbox wird als Cart angelegt.
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). Bluetooth-Scanner-Kopplung wird in Patrol gemockt, nicht real.
14. Nicht-Ziele
Abschnitt betitelt „14. Nicht-Ziele“| Nicht-Ziel | Begründung |
|---|---|
| WMS-Vollfunktion (Zonen, Bin-Locations, Pick-Paths im Zentrallager) | Werkszeit verwaltet Fahrzeug-Lager + Werkstatt, kein Hochregal-Lager. Kunden mit echtem WMS bleiben bei pfl, prenode oder SAP EWM. |
| RFID UHF (EPC Gen2) Gate-Scan | Hardware-Investment rechtfertigt sich nur ab 500+ MA mit stationären Gates. Nicht im Zielmarkt. |
| Freie Lieferanten-Ausschreibung / Preisspiegel | Liegt bei simplesystem, Mercateo, onventis. §4.6 konzentriert sich auf OCI-Punchout zum bestehenden Händler. |
| EDI-Auftragsbestätigung (ORDRSP, DESADV, INVOIC) | V2 Nicht-Scope, Nachfrage kommt erst bei Tier-1-Industrie-Kunden. |
| Chargenrückverfolgung über mehrere Jahre (Serialisierungs-Ledger) | §10.7 ElektroG-Basis reicht für Handwerk. Vollständige Serialisierung ist Pharma-/Tobacco-Thema. |
| Maschinenanbindung (OPC UA) | Liegt außerhalb des SaaS-Scope. |
| Automatische Rücknahme von Elektroschrott (WEEE-Konto) | Liegt bei den Erstinverkehrbringern (Hersteller). Werkszeit dokumentiert nur die Rücknahme als Bewegung. |
15. Risiken & offene Annahmen
Abschnitt betitelt „15. Risiken & offene Annahmen“| Risiko / Annahme | Impact | Wahrscheinlichkeit | Gegenmaßnahme |
|---|---|---|---|
| OCI-4.0-Varianten je Händler (Hagebau vs. Würth vs. Sonepar) sind nicht 100 % kompatibel | hoch | hoch | Eigener Adapter pro Händler, Interop-Test gegen Sandbox-Systeme vor jedem Release. Design-Partner pro Händler. |
| NFC-Tags werden beschädigt / ausgetauscht | mittel | mittel | Fallback manuelle Fahrzeug-Auswahl; monatlicher Tag-Reader-Health-Check. |
| §13b-UStG-Auslegung bei Material-Beistellung wechselt durch BFH-Urteil | hoch | niedrig | Export-Liste ist formatier-agnostisch, Steuerberater-Konsultation vor Go-Live mit Design-Partner. |
| Bluetooth-Scanner-Kopplung flaky auf Android 14 | mittel | mittel | Fallback Kamera-Scan, Kompatibilitäts-Matrix mit Zebra/Honeywell dokumentieren. |
| CRDT-Delta-Semantik verwirrt den Nutzer bei manueller Korrektur | mittel | mittel | UX-Interviews + Konflikt-Ticket zeigt klar „A bekam 5, Du bekamst 5, 3 offen“, nicht abstrakte Set-Differenz. |
| GoBD-Prüfer akzeptiert Hash-Chain nicht als „unveränderbar“ | hoch | niedrig | Zusätzlich S3 Object Lock Compliance-Mode für Foto-Belege + quartalsweiser WORM-Dump der stock_movements-Partition. |
| Auto-Lieferauftrag triggert Doppelbestellung bei Händler-Fehler | hoch | niedrig | Idempotency-Key auch zum Händler geschickt (X-Werkszeit-Order-Key); Manager-Freigabe über Schwellenwert. |
Offene Annahmen.
- NTAG213 reicht (180 Byte) vs. NTAG215 (504 Byte): hängt vom Umfang des NDEF-Records (Payload ohne Signatur ≈ 90 Byte, mit HMAC ≈ 140 Byte) ab — Entscheidung vor Bulk-Beschaffung.
- Der prozentuale Anteil offline-Entnahmen: angenommen 40 %, zu validieren mit erstem Design-Partner.
- Preisgenauigkeit bei OCI-Hook: Manche Händler liefern Brutto ohne MwSt-Breakdown — im Cart-Reconciliation-Layer abfangen.
16. Abhängigkeiten
Abschnitt betitelt „16. Abhängigkeiten“- Vorbedingung:
kern/02-auth-scopes(Scopesown/team/allals RLS-Policy-Fabrik).kern/08-audit-log(Hash-verkettete Append-only-Log-Infrastruktur).kern/05-s3-object-lock(presigned-URL-Multipart + Compliance-Mode-Config).handwerk/05-bau-abrechnung(Projekt-IDs als Kostenstellen — Bewegung schreibtprojectId).
- Schnittstelle zu:
- Bau-ERP-Integrationen (RIB iTWO, Nevaris) via BMEcat-Import.
- OCI-Händler (Hagebau, Würth, Sonepar, GC-Gruppe, Conrad Business).
- Stammdaten-Feeds ETIM, GS1 Germany.
- Wird konsumiert von:
handwerk/05-bau-abrechnung(Material-Ist-Kosten pro Projekt).handwerk/07-maengel-tickets(Material-Bezug bei Mangel-Behebung als Entnahme aufprojectId=<mängelprojekt>).kern/10-reporting(KPI-Karten Lager-Umschlag, ABC-Analyse).
17. Referenzkunde-Slot
Abschnitt betitelt „17. Referenzkunde-Slot“Status: TBD — zu klären mit Sales / Product-Owner.
DOR §1.1.1 verlangt: „Mindestens ein zahlender Design-Partner ist namentlich dokumentiert, der das Feature konkret fordert.“
Kandidaten-Profile:
- SHK-Betrieb NRW, 25–80 MA, 8–15 Service-Fahrzeuge, Würth- oder Hagebau-Stammkunde, Bestandsprobleme im wöchentlichen Betrieb.
- Elektro-Handwerk Bayern, 15–40 MA, Sonepar-Anschluss, hoher OCI-Reifegrad, existierender ETIM-Artikelstamm.
- Facility-Service NDS, 80–150 MA, mehrere Depots, Auto-Replenishment-Bedarf.
Validierungs-Fragen für das Erst-Gespräch:
- Wie viele Material-Entnahmen pro Fahrzeug und Tag (Median, Range)?
- Welcher Händler ist der wichtigste, mit welcher OCI-Version?
- Gab es im letzten Jahr Bestandsabweichungen, die Sie beim Steuerberater erklären mussten?
- Wie lange dauert Ihre aktuelle Inventur?
- Haben Sie schon NFC-Lösungen im Fahrzeug (z. B. LapID-Siegel)?
- Bereit, das Feature im Beta 90 Tage zu testen gegen 20 % Rabatt auf den Pro-Plan?
18. Senior-Berater-Empfehlung
Abschnitt betitelt „18. Senior-Berater-Empfehlung“Build-Sequenz.
- Datenmodell + RLS + Multi-Tenant-Test (
stock_movements,stock_levels,vehicles,vehicle_nfc_tags) — immer zuerst. - Backend-API
/movements+ OpenAPI-Spec → Dart-Client. - Mobile Scan-Flow (GS1-128 Parser + Kamera + BT-HID + Offline-Outbox).
- NFC-Check-in (Tag-Registration, Nonce-Protokoll).
- Web Bestandsübersicht + Konflikt-Panel.
- OCI-Integration Händler #1 (Empfehlung Würth, weil reifste OCI-4.0-Doku).
- Bestellvorschlag + Auto-Replenishment-Heuristik (noch keine ML).
- Inventur-Lauf + GoBD-Export.
- Compliance-Test-Suite: GS1-Muster, IDEA-ZIP, HMAC-Fail-Cases.
- E2E-Suite (Patrol-Multi-Device für Konflikt-Szenario).
- Doku-PR mit auto-generierten Screenshots.
Risiko-Reihenfolge (falls Zeitnot).
- Multi-Tenant-Isolation, Hash-Chain, Delta-Semantik, Idempotency sind nicht verhandelbar.
- Streich-Kandidaten: ML-Bestellvorschlag, Auto-Replenishment-Regeln (V2.5), Bluetooth-HID (Kamera reicht im MVP), BMEcat-Import (CSV-Import-First).
Stop-the-Bus-Triggers.
- Multi-Tenant-Bleed in einem Test.
- Hash-Chain-Bruch bei Konflikt-Auflösung.
- Idempotency-Key-Kollision führt zu Doppelbuchung.
- OCI-Hook akzeptiert gefälschte HMAC.
- GoBD-Prüfer in Design-Partner-Audit weist den Export zurück.
- Performance: Fahrzeug-Bestandsübersicht mit 500 Artikeln > 1,5 s p95 auf Seed-Tenant B.
Was uns 2027 dankbar macht.
- Delta-Semantik statt Set-Semantik. Jede spätere Erweiterung (Multi-Lager, Zonen, Chargen-Vererbung) ist ein weiteres Delta — nicht ein neues Update-Pattern. Migration zu einem echten CRDT-Typ bleibt möglich, weil die Oper-Struktur schon vorhanden ist.
- Idempotency-Key auf jeder Mutation. Retry-Flakiness (Android-Netz-Wechsel, iOS-Background-Kill) ist damit folgenlos. Alt-App hatte das nicht — siehe LESSONS-LEARNED §4.
- Hash-Chain ab Tag 1. GoBD-Prüfungen aus 2028 greifen auf 2026er Bewegungen zu — wir müssen das nachträglich nicht einbauen.
- OCI 4.0 von Anfang an. Wenn der Markt Peppol Catalog übernimmt, ist die Abstraktion „externer Warenkorb“ bereits vorhanden und der Adapter wird ausgetauscht, nicht das Modell.
Letzte Aktualisierung: 2026-04-19.
Für Entwickler — API-Endpoints4
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| GET | /v1/material/articles | bearerAuth | Artikel suchen (Volltext + GTIN-14-Lookup) |
| POST | /v1/material/movements | bearerAuth | Bestandsbewegung buchen (ISSUE / RETURN / CORRECTION / INVENTORY_SET) |
| POST | /v1/material/vehicles/{vehicleId}/checkin | bearerAuth | Fahrzeug per NFC anmelden (Nonce-Prüfung) |
| GET | /v1/material/vehicles/{vehicleId}/stock | bearerAuth | Bestandsübersicht eines Fahrzeugs |