feat(abschluss): interne Notiz des Fahrers beim Abschluss

- CompleteDeliveryAcknowledgements.internalNote (optional, getrimmt,
  max. 2000 Zeichen) wird atomar mit dem Abschluss gespeichert
  (delivery_completions.internal_note)
- Bewusst keine delivery_notes-Zeile: loest weder die Kunden-Bestaetigung
  der Notizen aus noch erscheint sie im Notizen-Step der App
- Lieferbericht: eigener Abschnitt "Interne Notiz" mit Zeitpunkt und Fahrer
- Bericht: Betrag in "Betrag erhalten" ans Zeilenende (Helvetica setzt nach
  dem Euro-Zeichen zu eng)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 14:41:05 +02:00
parent caaf88ac3d
commit 695ac39c19
9 changed files with 81 additions and 5 deletions

View File

@ -37,4 +37,9 @@ pub struct CompleteDeliveryAcknowledgements {
/// Fahrzeug des Akteurs (Audit-Spur). Muss zum Account gehören.
#[serde(default)]
pub author_car_id: Option<Uuid>,
/// Optionale interne Notiz des Fahrers, abgefragt nach beiden
/// Unterschriften. Nur für interne Zwecke (Lieferbericht), nicht Teil
/// der Kunden-Bestätigung. Leer/Whitespace = keine Notiz; max. 2000 Zeichen.
#[serde(default)]
pub internal_note: Option<String>,
}

View File

@ -111,6 +111,8 @@ pub struct ReportCompletion {
/// Die beim Abschluss gültige protokollierte Zahlung (None = ohne
/// Zahlungs-Step abgeschlossen, z. B. nichts offen oder alte App).
pub payment_id: Option<Uuid>,
/// Interne Notiz des Fahrers beim Abschluss (`None` = keine).
pub internal_note: Option<String>,
}
/// Ein Eintrag im Zahlungsprotokoll („Abkassieren").

View File

@ -76,6 +76,8 @@ pub struct CompleteDeliveryInput {
pub payment_method_id: Option<Uuid>,
pub completed_by_personalnummer: i64,
pub completed_by_car_id: Option<Uuid>,
/// Interne Notiz des Fahrers (bereits getrimmt, `None` = keine).
pub internal_note: Option<String>,
}
#[async_trait]

View File

@ -21,6 +21,10 @@ use crate::usecases::PushCompletionToErpUseCase;
/// Lock prüft). Schlägt das Repo-Gate fehl, bleiben höchstens die beiden
/// deterministisch benannten PNG-Dateien liegen — ein erneuter Versuch
/// überschreibt sie, es entsteht kein Müll.
/// Obergrenze der internen Abschluss-Notiz (Zeichen) — deckungsgleich mit
/// dem DB-CHECK in `0033_completion_internal_note.sql`.
const MAX_INTERNAL_NOTE_CHARS: usize = 2000;
pub struct CompleteDeliveryUseCase {
repository: Arc<dyn DeliveryCompletionRepository>,
signatures: Arc<dyn SignatureStorage>,
@ -69,6 +73,20 @@ impl CompleteDeliveryUseCase {
"driver signature is required".into(),
));
}
let internal_note = acknowledgements
.internal_note
.as_deref()
.map(str::trim)
.filter(|n| !n.is_empty())
.map(str::to_owned);
if internal_note
.as_ref()
.is_some_and(|n| n.chars().count() > MAX_INTERNAL_NOTE_CHARS)
{
return Err(ApplicationError::Validation(format!(
"internal note must not exceed {MAX_INTERNAL_NOTE_CHARS} characters"
)));
}
if let Some(car_id) = acknowledgements.author_car_id {
self.cars
.assert_owned_by_account(&[car_id], author_personalnummer)
@ -102,6 +120,7 @@ impl CompleteDeliveryUseCase {
payment_method_id: acknowledgements.payment_method_id,
completed_by_personalnummer: author_personalnummer,
completed_by_car_id: acknowledgements.author_car_id,
internal_note,
})
.await?;