Files
Holzleitner-Lieferservice-App/lib/domain/repository/tour_repository.dart
Dennis Nemec dd7fe9a8eb feat(zahlung): eigener Step "Zahlung" vor der Uebersicht
- Neuer Workflow-Step "Zahlung" zeigt ausgelieferte Artikel mit Menge und
  Preis, Betragsaufstellung, Zahlungsstatus und einen grossen Button
  "Zahlung abwickeln" (fest unten)
- Zahlungs-Modal: grosser offener Betrag, Methoden als Cards aus den
  Backend-Stammdaten, bei EC-Karte gross die Kundennummer; schliesst nur
  ueber Abbrechen, "Zahlung erhalten" oder "Lieferung abbrechen" (mit Grund)
- Bestaetigung wird am Server protokolliert (RecordDeliveryPayment); bis
  dahin sind "Weiter" und die Uebersicht gesperrt, ausser es ist nichts
  offen (bereits bezahlt). Aendert sich der Betrag danach, wird die Zahlung
  als veraltet markiert und muss erneut abgewickelt werden
- Uebersicht zeigt die abgewickelte Zahlung statt eigener Methodenauswahl;
  Unterschrift-Flow ohne doppelte Inkasso-Stufe; Abschluss sendet Methode
  und Inkasso-Flag der gueltigen Zahlung
- DeliveryPaymentStatus als einzige Quelle fuer offenen Betrag
  (gleiche Formel wie Backend); Artikelliste/Betragskarte als geteilte
  Widgets; Begruendungs-Dialog geteilt mit Info-Step
- API-Client aus aktueller Backend-Spec neu generiert
- Tests fuer Zahlungsstatus und Modal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:04:36 +02:00

228 lines
9.2 KiB
Dart

import 'package:hl_lieferservice/domain/entity/delivery.dart';
import 'package:hl_lieferservice/domain/entity/delivery_credit.dart';
import 'package:hl_lieferservice/domain/entity/delivery_payment.dart';
import 'package:hl_lieferservice/domain/entity/delivery_note.dart';
import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart';
import 'package:hl_lieferservice/domain/entity/scan_intent.dart';
import 'package:hl_lieferservice/domain/entity/tour.dart';
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
/// Port für das Tour-Aggregat.
///
/// Der Port deckt in dieser Migrations-Phase nur die Read-Seite + die
/// beiden Operationen, die zum Loading-Flow zwingend gebraucht werden:
/// Sortierung und Fahrzeug-Zuweisung. Hold/Resume/Cancel/Complete und
/// Notizen werden in C+D-4 nachgezogen, damit das hier nicht überladen
/// wird und der Bloc fokussiert bleibt.
///
/// Account-Filter sitzt serverseitig im JWT — der Client schickt nie eine
/// `personalnummer`/`accountId` mit.
abstract interface class TourRepository {
/// Die Tour-Übersicht des angemeldeten Fahrers für [date] (oder „heute",
/// wenn `null`) — bzw. `null`, wenn für den Tag keine Tour angelegt ist
/// (ERP-Sync noch nicht gelaufen, Fahrer-Urlaub etc.).
///
/// Ohne [date] entscheidet der Server über „heute" (autoritativ). Ist
/// [date] gesetzt (Fahrer wählt es im Kopf-Kalender), wird dieses Datum
/// angefragt.
///
/// Liefert nur die schlanke `TourSummary`-Repräsentation;
/// [getTourDetails] zieht dann den vollen Aggregat-Snapshot.
Future<TourSummary?> getMyTourSummary({DateTime? date});
/// Lädt das volle Tour-Aggregat (Tour + Lieferungen + Items +
/// Stammdaten + Notizen) für die gegebene Tour-Id.
Future<TourDetails> getTourDetails(String tourId);
/// Convenience: kombiniert [getMyTourSummary] + [getTourDetails] und gibt
/// `null` zurück, wenn für [date] (Default „heute") keine Tour existiert.
/// Verwendet die App beim Initial-Load und beim Datumswechsel.
Future<TourDetails?> getMyTourDetails({DateTime? date});
/// Schreibt die Sortier-Reihenfolge der Lieferungen einer Tour neu.
///
/// `orderedDeliveryIds` muss **alle** Lieferungen der Tour enthalten,
/// in der gewünschten Reihenfolge — das Backend lehnt unvollständige
/// Listen mit `400 validation` ab.
///
/// Rückgabe: deliveryId → neuer sortOrder (für den Bloc-Reducer).
Future<Map<String, int>> setDeliveryOrder({
required String tourId,
required List<String> orderedDeliveryIds,
});
/// Weist einer Lieferung ein Fahrzeug zu. `carId == null` löst die
/// bestehende Zuweisung. Der Server gibt die aktualisierte Delivery
/// zurück; weil dieser Endpoint nur Stamm-Felder mutiert, ist es Aufgabe
/// des Aufrufers, die `items` aus dem lokalen Aggregat zu erhalten.
///
/// Rückgabe: die Stamm-Delivery **ohne** Items — Aufrufer nutzt
/// `copyWith(items: ...)` zum Mergen mit dem lokalen State.
Future<Delivery> assignCarToDelivery({
required String deliveryId,
required String? carId,
});
/// Bricht eine Lieferung ab — endgültig (`canceled`). `reason` ist
/// vom Backend Pflicht; leere Begründungen werden mit 400 abgelehnt.
/// Rückgabe: Server-Snapshot der Delivery **ohne** Items.
Future<Delivery> cancelDelivery({
required String deliveryId,
required String reason,
});
/// Pausiert eine Lieferung (`held`). Reversibel über [resumeDelivery].
/// `reason` ist Pflicht.
Future<Delivery> holdDelivery({
required String deliveryId,
required String reason,
});
/// Setzt eine pausierte Lieferung auf `active` zurück. Kein Reason
/// erforderlich.
Future<Delivery> resumeDelivery({required String deliveryId});
/// Schließt eine Lieferung ab (`completed`). Lädt beide Unterschriften
/// (Kunde + Fahrer, PNG) per multipart hoch und dokumentiert die
/// Bestätigungen des Kunden. Atomar serverseitig — das Backend prüft
/// vorher: Lieferung aktiv, alle scanbaren Positionen fertig, Notizen
/// bestätigt (falls vorhanden). [paymentMethodId] persistiert die ggf. im
/// Summary geänderte Zahlungsmethode (muss existieren + aktiv sein); `null`
/// lässt die am Beleg hinterlegte Methode unangetastet. Rückgabe:
/// Server-Snapshot der Delivery **ohne** Items (Aufrufer merged Items aus
/// dem lokalen Aggregat).
Future<Delivery> completeDelivery({
required String deliveryId,
required List<int> customerSignaturePng,
required List<int> driverSignaturePng,
required bool receiptConfirmed,
required bool notesAcknowledged,
required List<String> acknowledgedNoteIds,
String? paymentMethodId,
String? actorCarId,
bool paymentCollected = false,
});
/// Legt eine neue Notiz an einer Lieferung an.
///
/// Mindestens eines von [text] und [imageAttachment] muss inhaltlich
/// gefüllt sein — das Backend erzwingt das. Aktuell unterstützt die App
/// nur den Text-Pfad; das `imageAttachment`-Feld bleibt der zukünftigen
/// Foto-Upload-Phase vorbehalten.
///
/// Rückgabe: die neu angelegte Notiz (mit Server-gesetzter `id` und
/// `createdAt`) — der Aufrufer hängt sie an das lokale Tour-Aggregat.
Future<DeliveryNote> addDeliveryNote({
required String deliveryId,
String? text,
String? imageAttachment,
String? creditDeliveryItemId,
bool isAmountCreditNote,
});
/// Ändert Text/Bild einer bestehenden Notiz. Mindestens eines von [text]
/// und [imageAttachment] muss inhaltlich gefüllt sein. Rückgabe: die
/// aktualisierte Notiz (Autor/`createdAt` bleiben).
Future<DeliveryNote> updateDeliveryNote({
required String deliveryId,
required String noteId,
String? text,
String? imageAttachment,
});
/// Löscht eine Notiz. Innerhalb des (geteilten) Accounts darf jeder Fahrer
/// löschen — keine Autor-Prüfung serverseitig.
Future<void> deleteDeliveryNote({
required String deliveryId,
required String noteId,
});
/// Lädt ein Bild zu einer Lieferung hoch (multipart, Feld `file`). Das
/// Backend reicht es an DOCUframe weiter und legt eine Bild-Notiz mit der
/// zurückgelieferten Referenz (`~ObjectID`) als `imageAttachment` an.
/// Rückgabe: die neue Notiz.
Future<DeliveryNote> uploadDeliveryNoteImage({
required String deliveryId,
required String filename,
required String mime,
required List<int> bytes,
});
/// Setzt/ändert die Betrags-Gutschrift einer Lieferung. Append-only +
/// idempotent über [clientEventId]. Rückgabe: aktueller Stand (`null`, wenn
/// — theoretisch — nichts gesetzt ist).
Future<DeliveryCredit?> setDeliveryCredit({
required String deliveryId,
required String clientEventId,
required int amountCents,
required String reason,
String? actorCarId,
});
/// Entfernt die Betrags-Gutschrift einer Lieferung (append-only `remove`).
/// Rückgabe: aktueller Stand danach (`null`).
Future<DeliveryCredit?> removeDeliveryCredit({
required String deliveryId,
required String clientEventId,
String? actorCarId,
});
/// Protokolliert die Zahlungsabwicklung („Abkassieren") einer Lieferung.
/// Append-only + idempotent über [clientEventId]. [expectedAmountCents] ist
/// der Betrag, den der Fahrer gesehen hat; weicht der Server-Betrag ab,
/// lehnt das Backend ab. Rückgabe: der protokollierte Eintrag.
Future<DeliveryPayment> recordDeliveryPayment({
required String deliveryId,
required String clientEventId,
required String paymentMethodId,
required int expectedAmountCents,
String? actorCarId,
});
/// Setzt (Upsert) den Wert eines Service für eine Lieferung. Genau das zum
/// Service-Typ passende Feld angeben. Rückgabe: der gespeicherte Wert.
Future<DeliveryServiceValue> setDeliveryService({
required String deliveryId,
required String serviceId,
bool? boolValue,
int? numericValue,
String? actorCarId,
});
/// Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt").
Future<void> removeDeliveryService({
required String deliveryId,
required String serviceId,
});
/// Wendet eine Liste Scan-Ereignisse als Batch am Server an.
///
/// Der Endpoint ist bewusst Bulk: damit kann der Client einen
/// Scanner-Burst (z. B. 5 Barcodes in 2 Sekunden) in einem HTTP-Call
/// abschicken, **muss** aber nicht — auch ein Aufruf mit nur einem
/// `ScanIntent` ist erlaubt.
///
/// Idempotenz: das Backend speichert pro `clientScanId` einmal. Wer
/// retried, bekommt `duplicate` zurück; doppelte Anwendung kann es
/// nicht geben.
///
/// Rückgabe: pro Eingabe-Intent ein [ScanOutcome] (Key =
/// `clientScanId`). Die Map enthält **jeden** Intent, auch
/// `rejected`-Fälle; bei Netzwerk-/Server-Fehlern wirft das Repository
/// stattdessen [TourRepositoryException], die Map ist dann nicht
/// teilweise gefüllt.
Future<Map<String, ScanOutcome>> applyScans(List<ScanIntent> intents);
}
/// Allgemeine Repository-Exception für Tour-Operationen. Konkrete Impls
/// dürfen spezifischere Subtypen werfen.
class TourRepositoryException implements Exception {
const TourRepositoryException(this.message, [this.cause]);
final String message;
final Object? cause;
@override
String toString() => 'TourRepositoryException: $message';
}