Files
Holzleitner-Lieferservice-App/docs/BACKEND_MIGRATION.md
Dennis Nemec 456fb59668 Phasenbasierte Lieferübersicht + Beladen-Flow, plus Migrationsplan für Rust-Backend
UI-Restructuring:
- TabBar in scan_page durch dedizierte Phasen ersetzt: Sortieren / Beladen / Ausliefern
- PhaseBloc + PhaseService leiten Phase aus Tour-/Item-States ab
- DeliverySelectionPage (ab 2 Autos) und DeliverySortPage als eigene Flows
- LoadingOverviewPage / LoadingCustomerPage für die Beladephase
- PhaseStepper-Widget im Home für Phasen-Anzeige
- Lager-Differenzierung (Standardlager 0 vs. Außenlager) via WarehouseBadge

Process-Stubs:
- ProcessRepository für Hold/Cancel/Sort/Assign-Flows (stub, bereit für Backend-Anbindung)

Doku:
- docs/BACKEND_MIGRATION.md: Phasenplan für Umstellung auf das neue
  Rust-Backend (OpenAPI-Generator, Keycloak OIDC, Clean-Arch-Layering)
2026-05-14 22:27:56 +02:00

8.5 KiB

Backend-Migration: ERPframe → Rust-Backend

Stand: 2026-05-14

Ziel

Die Holzleitner-Lieferservice-App wird vom alten ERPframe/DOCUframe-Backend auf das neue Rust-Backend (/Users/dennis/Desktop/Arbeit/Holzleitner-Backend) umgestellt. Auf Client-Seite kommt Clean Architecture konsequent durch: data-Layer (generierte API-DTOs), domain-Layer (App-Entities), Mapper, Repositories, Blocs.

Entscheidungen

Entscheidung Wahl Begründung
API-Client OpenAPI-Generator (dart mit dio als Provider) Wir haben sauberes OpenAPI im Backend; handschriftliche Services wären 1000 Zeilen Drift-Risiko.
Discount/Payment-Features Feature-Flag mit Stubs, UI-Pfade bleiben kompilierbar Diese Features existieren im neuen Backend nicht. Stubs werfen UnsupportedFeatureException. UI-Aufrufer können sie defensiv behandeln. Spätere Reaktivierung möglich, wenn Kunde bestätigt.
Offline-Scan-Queue Später, erst Online-Pfad Erstmal sauberer Online-Flow. Outbox-Pattern kommt als separate Iteration, wenn das Online-Verhalten sitzt.
Phasenfolge A → B → C → D → E → F Jede Phase einzeln smoke-getestet, bevor die nächste beginnt.

Backend-Endpunkte, die wir treffen

Aus dem fertigen Rust-Backend (Stand 2026-05-14):

HTTP Pfad Use Case
GET /me/tours/today Liste der heutigen Touren des angemeldeten Fahrers
GET /tours/{id} Tour-Aggregat (eine Lieferung pro Roundtrip)
PUT /tours/{id}/delivery-order Sortierreihenfolge schreiben
POST /scans Bulk-Scan-Endpoint mit clientScanId für Idempotenz
POST /deliveries/{id}/hold|resume|cancel|complete Lifecycle-Übergänge
PUT /deliveries/{id}/assigned-car Fahrzeug-Zuordnung
POST /deliveries/{id}/notes Notiz anlegen (Text/Bild-Anhang)
GET /me/cars[?includeInactive=true] Fahrzeuge des Fahrers
POST /me/cars Fahrzeug anlegen
PATCH /me/cars/{id} Fahrzeug ändern/deaktivieren
GET /accounts/{personalnummer} Account-Stammdaten

Auth: Keycloak OIDC, JWT Bearer, OIDC-Discovery via http://localhost:8080/realms/holzleitner/.well-known/openid-configuration, Audience holzleitner-api, Client holzleitner-app (public + PKCE).

Modell-Alignment

Die App-Modelle (lib/model/*) entsprechen nicht den Backend-Modellen. Wichtigste Differenzen, die der Mapper auflösen muss:

App heute Backend
Article.amount, scannedAmount, isParent, components[] DeliveryItem.requiredQuantity, scanState, flache Liste, komponentenArtikelNr?
DeliveryState.{ongoing, finished, onhold, canceled} DeliveryState.{active, completed, held, canceled}
Car { int id, String plate } Car { UUID id, i64 accountId, String plate, bool active }
Note { int id, String content } DeliveryNote { UUID, text?, imageAttachment?, authorPersonalnummer, authorCarId?, createdAt }
Tour { Driver driver, deliveriesPerCar } TourDetails { Tour, Vec<DeliveryWithItems>, customers, articles, warehouses, notes }
Discount { article, note, noteId } — kein Backend-Pendant
Payment { id, description, shortcode } — kein Backend-Pendant

Stücklisten-Komponenten als eigene Entity entfallen. Die Hierarchie kann die UI aus komponentenArtikelNr rekonstruieren, wenn gebraucht.

Was als Feature-Flag stubbt

Discount- und Payment-Pfade bleiben in der UI sichtbar, aber:

  • DiscountRepository.add/remove/updatethrow UnsupportedFeatureException("discounts")
  • PaymentRepository.list/select → liefert [] bzw. null
  • Aufrufende Stellen (z. B. TourBloc) fangen die Exception, zeigen Snackbar "Funktion nicht verfügbar" und machen weiter.

Konfigurierbar über FeatureFlags.discountsEnabled = false und FeatureFlags.paymentsEnabled = false (Default beides false).

Phasen

Phase A — Foundation

  • dio: ^5 und dio_smart_retry (optional) in pubspec.yaml.
  • openapi_generator als dev_dependency; Generator-Aufruf in tool/generate_api_client.sh (oder build_runner) verdrahten.
  • OpenAPI-Spec lokal aus dem laufenden Backend ziehen (curl http://localhost:3000/openapi.json -o openapi/holzleitner.json), einchecken (zur Reproduzierbarkeit), generierten Code unter lib/data/api/ ablegen, gitignoren.
  • AuthInterceptor für dio: Bearer-Header einhängen, bei 401 versuchen zu refreshen, sonst Logout-Event feuern.
  • get_it-Registrierungen für Dio, generierte API-Klassen, Repositories.

Smoke: GET /health und GET /me/cars (mit hartkodiertem Token aus Keycloak) via generiertem Client erreichbar.

Phase B — Auth (Keycloak OIDC)

  • flutter_appauth: ^7 für PKCE-Flow.
  • flutter_secure_storage für Refresh-Token.
  • KeycloakAuthService mit login(), logout(), refresh(), currentAccessToken().
  • LoginPage umbauen: kein Browser-Redirect + Deep-Link, sondern PKCE-Flow via flutter_appauth. Bei Erfolg: Tokens in Secure Storage, AuthBloc-Event SetAuthenticatedEvent.
  • AuthBloc.Authenticated State trägt jetzt { accessToken, refreshToken, idTokenClaims } statt sessionId.
  • AuthInterceptor aus Phase A nutzt diesen State.

Smoke: Login → Token landet im Secure Storage → GET /me/cars über Interceptor klappt.

Phase C — Domain & Mapper

  • Generierte DTOs (lib/data/api/model/) bleiben unverändert.
  • Domain-Entities in lib/domain/entity/ definieren — analog zu heute, aber an Backend-Felder angepasst (Enum-Werte, UUID-IDs etc.).
  • Mapper unter lib/data/mapper/ pro Entity: TourMapper.fromDto(TourDetailsDto) -> Tour.
  • lib/model/* und lib/dto/* werden gelöscht, sobald alle Aufrufer auf die neuen domain-Entities umgestellt sind. Bis dahin laufen beide parallel.

Kein UI-Test nötig — diese Phase ist reines Refactoring.

Phase D — Repositories

  • Pro Repository abstraktes Interface in lib/domain/repository/, konkrete Implementierung in lib/data/repository/.
  • Implementierungen rufen den generierten API-Client auf und mappen DTO ↔ Entity.
  • Neu/umgestellt:
    • TourRepositoryGET /me/tours/today + GET /tours/{id} + PUT /tours/{id}/delivery-order
    • DeliveryRepositoryPOST /deliveries/{id}/{hold,resume,cancel,complete}, PUT /deliveries/{id}/assigned-car
    • ScanRepositoryPOST /scans (bulk)
    • NoteRepositoryPOST /deliveries/{id}/notes
    • CarsRepositoryGET/POST/PATCH /me/cars
    • ProcessRepository (heute Stubs) → bekommt echte Calls
  • Wegfall:
    • DiscountRepository → Stub mit UnsupportedFeatureException
    • Payment-Methoden in TourRepository → Stub []

Smoke: Login → loadTourOfToday() → Tour kommt durch.

Phase E — Blocs

  • TourBloc: an neues Tour-Aggregat anpassen, Discount/Payment-Events hinter Feature-Flag.
  • CarsBloc: nutzt neue CarsRepository-Signaturen (UUID statt int, active statt delete).
  • CarSelectBloc: speichert Car-UUID statt int in SharedPreferences (Migration der bestehenden gespeicherten Auswahl: bei UUID-Parse-Fehler einfach null und neu auswählen lassen).
  • AuthBloc: bereits in Phase B angepasst, hier nur noch Konsistenz-Check.
  • PhaseBloc: bleibt UI-seitig, konsumiert das neue Aggregat.

Smoke: Tab-Navigation funktioniert, jeder Tab lädt ohne Crash.

Phase F — Smoke des kompletten Flows

Manuell durchklicken:

  1. Login (Keycloak)
  2. Fahrzeugauswahl
  3. Sortier-Phase (Reihenfolge per Drag & Drop, Persistieren)
  4. Beladen-Phase (Scan, Hold, Remove)
  5. Auslieferung-Phase (Complete, Notizen, Cancel)
  6. Logout

Bei jedem Schritt: Backend-Logs prüfen (richtige Endpoints gehen rein, keine 4xx/5xx).

Was die App nach der Migration nicht mehr kann

  • Rabatte (Discount-Pfade als Stub, UI zeigt Snackbar)
  • Zahlungsarten-Auswahl (Payment-Pfade als Stub)
  • Lieferung-spezifische ERP-Optionen (DeliveryOption) — fielen ohnehin durch das Modell raus

Diese Features kommen zurück, sobald sie im Backend modelliert sind.

Was die App nach der Migration neu kann

  • Idempotente Bulk-Scans (Offline-Wiederholbarkeit auf Protokoll-Ebene schon vorbereitet)
  • Saubere Audit-Spuren (actorCarId an Scans + Notizen)
  • Fahrzeug-Soft-Delete (statt Hardes Löschen)
  • Server-vergebene tour_date (statt App-Datum)

Out of Scope dieser Migration

  • Offline-Queue für Scans (kommt später)
  • Object-Storage für Bilder (heute schicken wir Strings, später pre-signed URLs)
  • Cars-Verwaltung von ERPframe migrieren — Fahrzeuge werden neu angelegt
  • ERP-Sync (POST /sync/tour) — das ist DOCUcontrol-Seite, nicht App