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:
@ -46,6 +46,7 @@ use holzleitner_application::usecases::{
|
||||
UpdateServiceUseCase, UploadDeliveryNoteImageUseCase,
|
||||
};
|
||||
use holzleitner_application::ports::DeliveryReportJobRepository;
|
||||
use holzleitner_application::ports::{SyncRunRepository, SyncTrigger};
|
||||
use holzleitner_infrastructure::auth::{
|
||||
KeycloakAdapterConfig, KeycloakAdminClient, KeycloakAdminConfig, KeycloakAuthService,
|
||||
};
|
||||
@ -60,7 +61,7 @@ use holzleitner_infrastructure::persistence::{
|
||||
PgAccountRepository, PgAttachmentRepository, PgCarRepository, PgDeliveryCompletionRepository,
|
||||
PgDeliveryCreditRepository, PgDeliveryNoteRepository, PgDeliveryReportJobRepository,
|
||||
PgDeliveryRepository, PgDeliveryServiceRepository, PgPaymentMethodRepository, PgScanRepository,
|
||||
PgServiceRepository, PgTourRepository, PoolConfig, connect_and_migrate,
|
||||
PgServiceRepository, PgSyncRunRepository, PgTourRepository, PoolConfig, connect_and_migrate,
|
||||
};
|
||||
use holzleitner_infrastructure::storage::{LocalAttachmentStorage, LocalSignatureStorage};
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
@ -214,6 +215,7 @@ pub(crate) async fn run_app(
|
||||
let delivery_service_repository = Arc::new(PgDeliveryServiceRepository::new(pool.clone()));
|
||||
let report_repository = Arc::new(PgDeliveryReportRepository::new(pool.clone()));
|
||||
let report_job_repository = Arc::new(PgDeliveryReportJobRepository::new(pool.clone()));
|
||||
let sync_run_repository = Arc::new(PgSyncRunRepository::new(pool.clone()));
|
||||
|
||||
// --- Lokaler Unterschriften-Speicher (Dateisystem) -----------------
|
||||
let signature_storage = Arc::new(
|
||||
@ -333,6 +335,7 @@ pub(crate) async fn run_app(
|
||||
erp_source,
|
||||
sync_tour.clone(),
|
||||
driver_provisioner,
|
||||
sync_run_repository.clone(),
|
||||
));
|
||||
// DEV-ONLY: überschreibender Resync (löscht Postgres-Tourdaten + Import).
|
||||
// Wird immer gebaut, der Endpoint aber nur bei dev.sync_enabled gemountet.
|
||||
@ -522,7 +525,7 @@ pub(crate) async fn run_app(
|
||||
let import = import.clone();
|
||||
Box::pin(async move {
|
||||
let date = (chrono::Utc::now() + chrono::Duration::days(offset)).date_naive();
|
||||
match import.execute(date).await {
|
||||
match import.execute_with(date, SyncTrigger::Scheduler).await {
|
||||
Ok(summary) => tracing::info!(
|
||||
date = %summary.date,
|
||||
total = summary.tours_total,
|
||||
@ -542,6 +545,48 @@ pub(crate) async fn run_app(
|
||||
offset_days = cfg.import.date_offset_days,
|
||||
"erp_import scheduler gestartet"
|
||||
);
|
||||
|
||||
// Startup-Catch-up: beim Serverstart nachsynchronisieren, falls für das
|
||||
// Zieldatum (heute + offset, i. d. R. morgen) noch KEIN erfolgreicher
|
||||
// Lauf dokumentiert ist. Deckt Erststart UND längere Unterbrechungen ab,
|
||||
// bei denen der Cron-Zeitpunkt (03:00) verpasst wurde. Läuft im
|
||||
// Hintergrund, damit der Serverstart nicht am (evtl. langsamen oder
|
||||
// abwesenden) ERP hängt.
|
||||
{
|
||||
let import = import_erp_tours.clone();
|
||||
let sync_runs = sync_run_repository.clone();
|
||||
let offset = cfg.import.date_offset_days;
|
||||
tokio::spawn(async move {
|
||||
let date = (chrono::Utc::now() + chrono::Duration::days(offset)).date_naive();
|
||||
match sync_runs.has_successful_run_for(date).await {
|
||||
Ok(true) => tracing::info!(
|
||||
%date,
|
||||
"erp_import.catchup: bereits erfolgreich gesynct — übersprungen"
|
||||
),
|
||||
Ok(false) => {
|
||||
tracing::info!(%date, "erp_import.catchup: kein Sync vorhanden — synchronisiere nach");
|
||||
match import.execute_with(date, SyncTrigger::Startup).await {
|
||||
Ok(s) => tracing::info!(
|
||||
date = %s.date,
|
||||
total = s.tours_total,
|
||||
ok = s.tours_ok,
|
||||
failed = s.tours_failed,
|
||||
"erp_import.catchup.done"
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::error!(%date, error = %e, "erp_import.catchup.failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!(
|
||||
%date,
|
||||
error = %e,
|
||||
"erp_import.catchup: Statusabfrage fehlgeschlagen"
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some(scheduler)
|
||||
} else {
|
||||
tracing::info!("erp_import deaktiviert (IMPORT_ENABLED!=true)");
|
||||
|
||||
Reference in New Issue
Block a user