feat(import): ERP-Sync in DB dokumentieren + Startup-Catch-up

Jeder ERPframe-Import (Scheduler, Startup, manuell) wird in der neuen Tabelle
erp_sync_runs protokolliert (target_date, trigger, success, Zaehler, Fehler).
Beim Serverstart synct das Backend das Zieldatum (heute + offset, i.d.R. morgen)
nach, falls dafuer noch kein erfolgreicher Lauf dokumentiert ist — deckt
Erststart UND laengere Unterbrechungen ab, bei denen der Cron-Zeitpunkt verpasst
wurde. Gated ueber [import] enabled.

- Migration 0030_erp_sync_runs
- Port SyncRunRepository (+ SyncRunRecord, SyncTrigger)
- Adapter PgSyncRunRepository
- ImportErpToursUseCase dokumentiert jeden Lauf; neues execute_with(date, trigger)
- main.rs: Repo verdrahtet, Scheduler-Trigger, Startup-Catch-up (tokio::spawn)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-06-08 16:23:12 +02:00
parent c65e13485d
commit 2f4368ec52
7 changed files with 272 additions and 3 deletions

View File

@ -17,6 +17,7 @@ pub mod payment_method_repository;
pub mod pool;
pub mod scan_repository;
pub mod service_repository;
pub mod sync_run_repository;
pub mod tour_repository;
pub use account_repository::PgAccountRepository;
@ -32,4 +33,5 @@ pub use payment_method_repository::PgPaymentMethodRepository;
pub use pool::{connect_and_migrate, PoolConfig};
pub use scan_repository::PgScanRepository;
pub use service_repository::PgServiceRepository;
pub use sync_run_repository::PgSyncRunRepository;
pub use tour_repository::PgTourRepository;

View File

@ -0,0 +1,60 @@
//! Postgres-Implementierung von `SyncRunRepository` (Tabelle `erp_sync_runs`).
//!
//! Reines Append-/Audit-Log der ERP-Import-Läufe + die Catch-up-Abfrage
//! „gibt es schon einen erfolgreichen Sync für Datum X?".
use async_trait::async_trait;
use chrono::NaiveDate;
use sqlx::PgPool;
use holzleitner_application::error::ApplicationError;
use holzleitner_application::ports::{SyncRunRecord, SyncRunRepository};
pub struct PgSyncRunRepository {
pool: PgPool,
}
impl PgSyncRunRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
fn db<E: std::fmt::Display>(e: E) -> ApplicationError {
ApplicationError::Repository(e.to_string())
}
#[async_trait]
impl SyncRunRepository for PgSyncRunRepository {
async fn record(&self, run: SyncRunRecord) -> Result<(), ApplicationError> {
sqlx::query(
"INSERT INTO erp_sync_runs \
(target_date, trigger, success, tours_total, tours_ok, tours_failed, \
drivers_provisioned, error) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
)
.bind(run.target_date)
.bind(run.trigger.as_str())
.bind(run.success)
.bind(run.tours_total)
.bind(run.tours_ok)
.bind(run.tours_failed)
.bind(run.drivers_provisioned)
.bind(run.error)
.execute(&self.pool)
.await
.map_err(db)?;
Ok(())
}
async fn has_successful_run_for(&self, date: NaiveDate) -> Result<bool, ApplicationError> {
let exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM erp_sync_runs WHERE target_date = $1 AND success)",
)
.bind(date)
.fetch_one(&self.pool)
.await
.map_err(db)?;
Ok(exists)
}
}