diff --git a/src/worker/politics.rs b/src/worker/politics.rs index 1a75c3f..13ec639 100644 --- a/src/worker/politics.rs +++ b/src/worker/politics.rs @@ -10,6 +10,7 @@ use crate::worker::sql::{ QUERY_COUNT_OFFICES_PER_REGION, QUERY_FIND_OFFICE_GAPS, QUERY_SELECT_NEEDED_ELECTIONS, + QUERY_ENFORCE_ELECTION_LEAD_TIME, QUERY_INSERT_CANDIDATES, QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES, QUERY_PROCESS_EXPIRED_AND_FILL, @@ -181,10 +182,31 @@ impl PoliticsWorker { Self::notify_office_filled(pool, broker, &new_offices_direct)?; } - // 5) Neue Wahlen planen + // 4b) Nach dem Entfernen abgelaufener Aemter erneut Luecken + // synchronisieren. Das behebt Rueckstaende, wenn Aemter schon vor + // Tagen/Wochen abgelaufen sind und dadurch erst jetzt echte Gaps + // entstehen. + Self::sync_offices_with_types(pool)?; + + // 5) Sicherstellen, dass Wahlen niemals vor Ablauf von 2 Tagen seit + // Anlegen ausgewertet werden. + Self::enforce_election_lead_time(pool)?; + + // 6) Bereits faellige Wahlen auswerten und neu besetzte Aemter melden. + // Wichtig: Vor dem Anlegen neuer Wahlen ausfuehren, damit frisch + // angelegte Wahlen nicht im selben Tageslauf wieder verarbeitet werden. + let new_offices_from_elections = Self::process_elections(pool)?; + if let Err(e) = Self::notify_player_election_results(pool, broker) { + eprintln!("[PoliticsWorker] notify_player_election_results: {e}"); + } + if !new_offices_from_elections.is_empty() { + Self::notify_office_filled(pool, broker, &new_offices_from_elections)?; + } + + // 7) Neue Wahlen planen let new_elections = Self::schedule_elections(pool)?; - // 6) Für alle Wahlen ohne Kandidaten (inkl. manuell + // 8) Fuer alle Wahlen ohne Kandidaten (inkl. manuell // angelegter) Kandidaten eintragen. let mut elections_to_fill = Self::find_elections_needing_candidates(pool)?; // Die gerade neu angelegten Wahlen sind typischerweise auch @@ -207,16 +229,7 @@ impl PoliticsWorker { Self::notify_election_created(pool, broker, &user_ids)?; } - // 7) Wahlen auswerten und neu besetzte Ämter melden - let new_offices_from_elections = Self::process_elections(pool)?; - if let Err(e) = Self::notify_player_election_results(pool, broker) { - eprintln!("[PoliticsWorker] notify_player_election_results: {e}"); - } - if !new_offices_from_elections.is_empty() { - Self::notify_office_filled(pool, broker, &new_offices_from_elections)?; - } - - // 8) Als letzter Schritt sicherstellen, dass es für keinen + // 9) Als letzter Schritt sicherstellen, dass es fuer keinen // Amtstyp/Region-Kombi mehr besetzte Ämter gibt als laut // `seats_per_region` erlaubt. Dieser Abgleich wird nach allen // Lösch- und Besetzungsvorgängen ausgeführt. @@ -337,8 +350,8 @@ impl PoliticsWorker { return Ok(()); } - // Für jede Lücke eine Wahl am aktuellen Tag anlegen, sofern es nicht - // bereits eine Wahl für diesen Amtstyp/Region an einem heutigen oder + // Für jede Lücke eine Wahl mit 2 Tagen Vorlauf anlegen, sofern es nicht + // bereits eine Wahl für diesen Amtstyp/Region an einem gleich späten oder // zukünftigen Datum gibt. Das eigentliche Anlegen der Wahl und das // Eintragen von Kandidaten läuft weiterhin über die bestehende // Logik in der Datenbank (process_elections / schedule_elections). @@ -353,7 +366,7 @@ impl PoliticsWorker { (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) SELECT $1::int AS office_type_id, - CURRENT_DATE AS date, + (CURRENT_DATE + INTERVAL '2 days')::date AS date, $2::int AS posts_to_fill, NOW() AS created_at, NOW() AS updated_at, @@ -363,7 +376,7 @@ impl PoliticsWorker { FROM falukant_data.election e WHERE e.office_type_id = $1::int AND e.region_id = $3::int - AND e.date::date >= CURRENT_DATE + AND e.date::date >= (CURRENT_DATE + INTERVAL '2 days')::date ); "#; @@ -634,6 +647,19 @@ impl PoliticsWorker { .collect()) } + fn enforce_election_lead_time(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("enforce_election_lead_time", QUERY_ENFORCE_ELECTION_LEAD_TIME) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare enforce_election_lead_time: {e}")))?; + conn.execute("enforce_election_lead_time", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec enforce_election_lead_time: {e}")))?; + + Ok(()) + } + /// Benachrichtigt Spieler-Kandidaten mit Wahlergebnis (Stimmen, Rang, Gewinner, gewonnen?). /// `tr` = JSON mit `event: "election_result"` (siehe `QUERY_PLAYER_ELECTION_RESULT_ROWS`). fn notify_player_election_results( diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 43fd4b7..d7b5b08 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1113,7 +1113,7 @@ pub const QUERY_FIND_OFFICE_GAPS: &str = r#" pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" WITH target_date AS ( - SELECT NOW()::date AS election_date + SELECT (NOW()::date + INTERVAL '2 days')::date AS election_date ), offices_ending_today AS ( SELECT po.id, @@ -1186,6 +1186,14 @@ pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" ORDER BY ne.region_id, ne.election_id; "#; + pub const QUERY_ENFORCE_ELECTION_LEAD_TIME: &str = r#" + UPDATE falukant_data.election AS e + SET date = (e.created_at::date + INTERVAL '2 days')::date, + updated_at = NOW() + WHERE e.created_at IS NOT NULL + AND e.date::date < (e.created_at::date + INTERVAL '2 days')::date; + "#; + pub const QUERY_INSERT_CANDIDATES: &str = r#" INSERT INTO falukant_data.candidate (election_id, character_id, created_at, updated_at)