feat(tours): GET /me/tours mit optionalem ?date= (statt /me/tours/today)

Fahrer können jetzt die Tour eines beliebigen Datums abrufen (z. B. morgen
vorab). Ohne date-Param bleibt serverseitig 'heute' (Local::now bzw.
Dev-today_override); Praezedenz: requested_date > today_override > Uhr.
Endpoint umbenannt /me/tours/today -> /me/tours (semantisch sauberer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-01 15:41:28 +02:00
parent 1f63b828d8
commit 17dcc42291
5 changed files with 56 additions and 19 deletions

View File

@ -460,7 +460,7 @@ pub struct GsdConfig {
#[derive(Debug, Clone, Deserialize, Default)] #[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct DevConfig { pub struct DevConfig {
/// Überschreibt das serverseitige „heute" für `GET /me/tours/today`. /// Überschreibt das serverseitige „heute" für `GET /me/tours`.
/// Als **quotierter** String `"YYYY-MM-DD"` angeben — ein bloßes /// Als **quotierter** String `"YYYY-MM-DD"` angeben — ein bloßes
/// `2026-06-01` würde TOML als nativen Datums-Typ lesen, den chrono /// `2026-06-01` würde TOML als nativen Datums-Typ lesen, den chrono
/// nicht direkt akzeptiert. `None`/weglassen = echte Uhr. Nur zum /// nicht direkt akzeptiert. `None`/weglassen = echte Uhr. Nur zum

View File

@ -22,7 +22,7 @@ use utoipa::openapi::security::{
paths( paths(
crate::routes::health::health, crate::routes::health::health,
crate::routes::accounts::get_account, crate::routes::accounts::get_account,
crate::routes::tours::list_my_tours_today, crate::routes::tours::list_my_tours,
crate::routes::tours::get_tour, crate::routes::tours::get_tour,
crate::routes::tours::set_delivery_order, crate::routes::tours::set_delivery_order,
crate::routes::tours::sync_tour, crate::routes::tours::sync_tour,

View File

@ -1,11 +1,13 @@
use axum::Json; use axum::Json;
use axum::Router; use axum::Router;
use axum::extract::{Path, State}; use axum::extract::{Path, Query, State};
use axum::routing::{get, post, put}; use axum::routing::{get, post, put};
use chrono::NaiveDate;
use holzleitner_application::dto::{ use holzleitner_application::dto::{
SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary, SetDeliveryOrderRequest, SetDeliveryOrderResponse, SyncTourRequest, TourDetails, TourSummary,
}; };
use serde::Serialize; use holzleitner_application::error::ApplicationError;
use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::error::ApiError; use crate::error::ApiError;
@ -14,7 +16,7 @@ use crate::state::AppState;
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/me/tours/today", get(list_my_tours_today)) .route("/me/tours", get(list_my_tours))
.route("/tours/{tour_id}", get(get_tour)) .route("/tours/{tour_id}", get(get_tour))
.route("/tours/{tour_id}/delivery-order", put(set_delivery_order)) .route("/tours/{tour_id}/delivery-order", put(set_delivery_order))
.route("/sync/tour", post(sync_tour)) .route("/sync/tour", post(sync_tour))
@ -37,27 +39,52 @@ pub struct SyncTourResponse {
pub tour_id: Uuid, pub tour_id: Uuid,
} }
/// Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). /// Query für `GET /me/tours`. Optionales Ziel-Datum; fehlt es, gilt
/// serverseitig „heute" (siehe `ListMyToursTodayUseCase`).
#[derive(Debug, Deserialize)]
pub struct ListToursQuery {
/// Ziel-Tourdatum `YYYY-MM-DD`. Fehlt der Parameter, wird **heute**
/// (Serverzeit bzw. Dev-Override) verwendet.
#[serde(default)]
pub date: Option<String>,
}
/// Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein
/// Ziel-Datum. Ohne `?date=` gilt „heute". Der Fahrer wählt das Datum aktiv
/// im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen).
#[utoipa::path( #[utoipa::path(
get, get,
path = "/me/tours/today", path = "/me/tours",
tag = "tours", tag = "tours",
params(
("date" = Option<String>, Query, description = "Ziel-Tourdatum YYYY-MM-DD (Default: heute)")
),
responses( responses(
(status = 200, description = "Liste der heutigen Touren", body = TourSummaryList), (status = 200, description = "Liste der Touren am Ziel-Datum", body = TourSummaryList),
(status = 400, description = "Ungültiges Datum"),
(status = 401, description = "Authentifizierung fehlgeschlagen") (status = 401, description = "Authentifizierung fehlgeschlagen")
), ),
security( security(
("bearer_auth" = []) ("bearer_auth" = [])
) )
)] )]
pub async fn list_my_tours_today( pub async fn list_my_tours(
State(state): State<AppState>, State(state): State<AppState>,
AuthenticatedUser(claims): AuthenticatedUser, AuthenticatedUser(claims): AuthenticatedUser,
Query(query): Query<ListToursQuery>,
) -> Result<Json<TourSummaryList>, ApiError> { ) -> Result<Json<TourSummaryList>, ApiError> {
tracing::debug!(personalnummer = claims.personalnummer, "list_my_tours_today"); let date = match query.date {
Some(s) => Some(NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d").map_err(|e| {
ApiError(ApplicationError::Validation(format!(
"ungültiges Datum '{s}' (erwartet YYYY-MM-DD): {e}"
)))
})?),
None => None,
};
tracing::debug!(personalnummer = claims.personalnummer, ?date, "list_my_tours");
let tours = state let tours = state
.list_my_tours_today .list_my_tours_today
.execute(claims.personalnummer) .execute(claims.personalnummer, date)
.await?; .await?;
Ok(Json(TourSummaryList { tours })) Ok(Json(TourSummaryList { tours }))
} }

View File

@ -1,4 +1,4 @@
//! Liste-Antwort für `GET /me/tours/today` — leichtgewichtig, ohne //! Liste-Antwort für `GET /me/tours` — leichtgewichtig, ohne
//! Lieferpositionen. Die App nutzt diese Liste für die //! Lieferpositionen. Die App nutzt diese Liste für die
//! Fahrzeug/Tour-Auswahl-Page und lädt erst nach der Auswahl die //! Fahrzeug/Tour-Auswahl-Page und lädt erst nach der Auswahl die
//! vollständige Tour über `GET /tours/{id}`. //! vollständige Tour über `GET /tours/{id}`.

View File

@ -6,9 +6,15 @@ use crate::dto::TourSummary;
use crate::error::ApplicationError; use crate::error::ApplicationError;
use crate::ports::TourRepository; use crate::ports::TourRepository;
/// Liste der heutigen Touren des angemeldeten Fahrers. Das "heute" /// Liste der Touren des angemeldeten Fahrers für ein Ziel-Datum.
/// liegt **bewusst im Backend**: die App-Uhr ist nicht autoritativ ///
/// (Zeitzone, Falsch-Stand, Manipulation). /// Ohne explizites Datum gilt "heute" — und das liegt **bewusst im
/// Backend**: die App-Uhr ist nicht autoritativ (Zeitzone, Falsch-Stand,
/// Manipulation). Übergibt die App ein Datum (Fahrer wählt es aktiv im
/// Kopf-Kalender, z. B. um die Tour von morgen vorab zu sehen), gewinnt
/// dieses vor der Uhr.
///
/// Präzedenz: **`requested_date` > `today_override` > echte Uhr**.
/// ///
/// `today_override` ist eine **DEV-ONLY**-Hintertür zum Testen mit /// `today_override` ist eine **DEV-ONLY**-Hintertür zum Testen mit
/// historischen/importierten Touren: ist sie gesetzt, wird statt der echten /// historischen/importierten Touren: ist sie gesetzt, wird statt der echten
@ -26,17 +32,21 @@ impl ListMyToursTodayUseCase {
} }
} }
pub async fn execute(&self, personalnummer: i64) -> Result<Vec<TourSummary>, ApplicationError> { pub async fn execute(
&self,
personalnummer: i64,
requested_date: Option<NaiveDate>,
) -> Result<Vec<TourSummary>, ApplicationError> {
// Bewusst Local statt Utc: der Tagesumschlag muss sich nach der // Bewusst Local statt Utc: der Tagesumschlag muss sich nach der
// Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um // Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um
// 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach // 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach
// Mitternacht noch die Touren von gestern. Der Server läuft in // Mitternacht noch die Touren von gestern. Der Server läuft in
// Europe/Vienna, daher ist Local hier korrekt. // Europe/Vienna, daher ist Local hier korrekt.
let today = self let date = requested_date
.today_override .or(self.today_override)
.unwrap_or_else(|| Local::now().date_naive()); .unwrap_or_else(|| Local::now().date_naive());
self.repository self.repository
.find_today_for_driver(personalnummer, today) .find_today_for_driver(personalnummer, date)
.await .await
} }
} }