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

@ -215,8 +215,10 @@ pub(crate) async fn run_app(
let car_repository = Arc::new(PgCarRepository::new(pool.clone()));
let payment_method_repository = Arc::new(PgPaymentMethodRepository::new(pool.clone()));
let delivery_credit_repository = Arc::new(PgDeliveryCreditRepository::new(pool.clone()));
let delivery_completion_repository =
Arc::new(PgDeliveryCompletionRepository::new(pool.clone()));
let delivery_completion_repository = Arc::new(PgDeliveryCompletionRepository::new(
pool.clone(),
cfg.server.timezone,
));
let service_repository = Arc::new(PgServiceRepository::new(pool.clone()));
let delivery_service_repository = Arc::new(PgDeliveryServiceRepository::new(pool.clone()));
let report_repository = Arc::new(PgDeliveryReportRepository::new(pool.clone()));
@ -246,7 +248,7 @@ pub(crate) async fn run_app(
);
let generate_delivery_report = Arc::new(GenerateDeliveryReportUseCase::new(
report_repository,
Arc::new(PdfDeliveryReportRenderer),
Arc::new(PdfDeliveryReportRenderer::new(cfg.server.timezone)),
report_sink.clone(),
signature_storage.clone(),
attachment_storage.clone(),
@ -301,6 +303,7 @@ pub(crate) async fn run_app(
let list_my_tours_today = Arc::new(ListMyToursTodayUseCase::new(
tour_repository.clone(),
cfg.dev.today_override,
cfg.server.timezone,
));
let sync_tour = Arc::new(SyncTourUseCase::new(tour_repository.clone()));
// ERP-Import (täglicher Pull aus ERPframe-MSSQL) — nutzt denselben
@ -465,6 +468,7 @@ pub(crate) async fn run_app(
sync_tour,
set_delivery_order,
import_erp_tours: import_erp_tours.clone(),
timezone: cfg.server.timezone,
dev_resync_tours,
dev_reset_delivery,
generate_delivery_report,
@ -556,10 +560,14 @@ pub(crate) async fn run_app(
.context("JobScheduler konnte nicht erstellt werden")?;
let import = import_erp_tours.clone();
let offset = cfg.import.date_offset_days;
let tz = cfg.server.timezone;
let job = Job::new_async(cfg.import.cron.as_str(), move |_uuid, _lock| {
let import = import.clone();
Box::pin(async move {
let date = (chrono::Local::now() + chrono::Duration::days(offset)).date_naive();
// Kalendertag in der konfigurierten Zone (nicht OS-Local).
let date = (chrono::Utc::now().with_timezone(&tz)
+ chrono::Duration::days(offset))
.date_naive();
match import.execute_with(date, SyncTrigger::Scheduler).await {
Ok(summary) => tracing::info!(
date = %summary.date,
@ -591,8 +599,11 @@ pub(crate) async fn run_app(
let import = import_erp_tours.clone();
let sync_runs = sync_run_repository.clone();
let offset = cfg.import.date_offset_days;
let tz = cfg.server.timezone;
tokio::spawn(async move {
let date = (chrono::Local::now() + chrono::Duration::days(offset)).date_naive();
let date = (chrono::Utc::now().with_timezone(&tz)
+ chrono::Duration::days(offset))
.date_naive();
match sync_runs.has_successful_run_for(date).await {
Ok(true) => tracing::info!(
%date,