use std::sync::Arc; use uuid::Uuid; use holzleitner_domain::DeliveryCredit; use crate::dto::{CreditAction, DeliveryCreditEventRequest}; use crate::error::ApplicationError; use crate::ports::{CarRepository, DeliveryCreditRepository}; /// Obergrenze der Betrags-Gutschrift in Cent (150 €). const MAX_CREDIT_CENTS: i64 = 15_000; /// Wendet ein Gutschrift-Ereignis (`set`/`remove`) auf eine Lieferung an. /// /// Validierung (fachlich, ohne DB): /// * `Set`: Betrag Pflicht, `0 < amount ≤ 150 €` (beliebiger Betrag, keine /// Schrittweite); Begründung Pflicht (nicht leer). /// * `author_car_id` muss — falls gesetzt — zum Account gehören. /// /// Den `active`-Check der Lieferung und die Idempotenz (`client_event_id`) /// übernimmt das Repository mit der gelockten Zeile. pub struct ApplyDeliveryCreditEventUseCase { repository: Arc, cars: Arc, } impl ApplyDeliveryCreditEventUseCase { pub fn new( repository: Arc, cars: Arc, ) -> Self { Self { repository, cars } } pub async fn execute( &self, delivery_id: Uuid, author_personalnummer: i64, request: DeliveryCreditEventRequest, ) -> Result, ApplicationError> { if let Some(car_id) = request.author_car_id { self.cars .assert_owned_by_account(&[car_id], author_personalnummer) .await?; } let (amount_cents, reason) = match request.action { CreditAction::Set => { let amount = request.amount_cents.ok_or_else(|| { ApplicationError::Validation("amount_cents required for set".into()) })?; if amount <= 0 || amount > MAX_CREDIT_CENTS { return Err(ApplicationError::Validation(format!( "amount_cents must be in (0, {MAX_CREDIT_CENTS}]" ))); } let reason = request .reason .as_deref() .map(str::trim) .filter(|s| !s.is_empty()) .ok_or_else(|| { ApplicationError::Validation("reason required for set".into()) })? .to_owned(); (amount, Some(reason)) } // Remove: Betrag/Grund irrelevant. CreditAction::Remove => (0, None), }; self.repository .apply_event( delivery_id, request.client_event_id, request.action, amount_cents, reason, author_personalnummer, request.author_car_id, ) .await } }