fix(time): konfigurierbare Fach-Zeitzone statt UTC/OS-Local fuer alle Datumsangaben

Lieferbericht zeigte UTC (08.09. 22:54 statt 09.09. 00:54): renderer.dt()
formatierte DateTime<Utc> ohne Umrechnung. Weitere Stellen hingen an
chrono::Local (OS-Zone des Hosts) bzw. hardcodiertem 'Europe/Berlin'.

Neu: server.timezone (IANA, Default Europe/Berlin, chrono-tz) und
durchgaengig UTC-Instant -> konfigurierte Zone:
- Lieferbericht: alle Zeitstempel (Abgeschlossen, Notizen, Scans, erzeugt am)
- Belegansicht (fmt_datetime)
- ERP-Rueckschreibung delivered_at (Wanduhrzeit)
- 'heute'-Ableitung: /me/tours, /dev/resync, /admin/import-erp,
  Import-Cron + Startup-Catch-up
- Completion-Repo: AT TIME ZONE als Bind-Param statt Literal

Speicherung bleibt UTC (TIMESTAMPTZ / DateTime<Utc>) - nur Anzeige und
Kalendertag-Ableitung nutzen die Zone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-09 01:11:06 +02:00
parent 54ed619396
commit f74b6c608a
14 changed files with 154 additions and 47 deletions

View File

@ -17,6 +17,7 @@ async-trait.workspace = true
thiserror.workspace = true
uuid.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
sha2.workspace = true
imagesize.workspace = true
utoipa = { workspace = true, optional = true }

View File

@ -1,6 +1,7 @@
use std::sync::Arc;
use chrono::{Local, NaiveDate};
use chrono::{NaiveDate, Utc};
use chrono_tz::Tz;
use crate::dto::TourSummary;
use crate::error::ApplicationError;
@ -22,13 +23,20 @@ use crate::ports::TourRepository;
pub struct ListMyToursTodayUseCase {
repository: Arc<dyn TourRepository>,
today_override: Option<NaiveDate>,
/// Fach-Zeitzone (`server.timezone`), in der „heute" bestimmt wird.
tz: Tz,
}
impl ListMyToursTodayUseCase {
pub fn new(repository: Arc<dyn TourRepository>, today_override: Option<NaiveDate>) -> Self {
pub fn new(
repository: Arc<dyn TourRepository>,
today_override: Option<NaiveDate>,
tz: Tz,
) -> Self {
Self {
repository,
today_override,
tz,
}
}
@ -37,14 +45,15 @@ impl ListMyToursTodayUseCase {
personalnummer: i64,
requested_date: Option<NaiveDate>,
) -> Result<Vec<TourSummary>, ApplicationError> {
// Bewusst Local statt Utc: der Tagesumschlag muss sich nach der
// Ortszeit des Fahrers richten. Mit Utc wechselt "heute" erst um
// 02:00 Ortszeit (CEST = UTC+2) — bis dahin sähe der Fahrer nach
// Mitternacht noch die Touren von gestern. Der Server läuft in
// Europe/Vienna, daher ist Local hier korrekt.
// Der Tagesumschlag muss sich nach der Ortszeit des Fahrers richten:
// mit reinem Utc wechselte "heute" erst um 02:00 Ortszeit (CEST =
// UTC+2) — bis dahin sähe der Fahrer nach Mitternacht noch die Touren
// von gestern. Daher der UTC-Instant → konfigurierte Fach-Zeitzone
// (`server.timezone`) → Kalendertag. Bewusst NICHT `chrono::Local`:
// das wäre die OS-Zone des Hosts und damit einstellungsabhängig.
let date = requested_date
.or(self.today_override)
.unwrap_or_else(|| Local::now().date_naive());
.unwrap_or_else(|| Utc::now().with_timezone(&self.tz).date_naive());
self.repository
.find_today_for_driver(personalnummer, date)
.await