diff --git a/src/worker/director.rs b/src/worker/director.rs index 3a6d14b..daec39d 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -1,4 +1,5 @@ use crate::db::{DbConnection, DbError, Row}; +use rand::Rng; use std::collections::{HashMap, HashSet}; use crate::message_broker::MessageBroker; use std::sync::atomic::Ordering; @@ -26,6 +27,8 @@ use crate::worker::sql::{ QUERY_SET_SALARY_PAYED, QUERY_UPDATE_SATISFACTION, QUERY_GET_DIRECTOR_USER, + QUERY_GET_DIRECTORS_FOR_RESIGNATION_CHECK, + QUERY_DELETE_DIRECTOR_BY_ID, QUERY_COUNT_VEHICLES_IN_BRANCH_REGION, QUERY_COUNT_VEHICLES_IN_REGION, QUERY_CHECK_ROUTE, @@ -36,6 +39,7 @@ use crate::worker::sql::{ QUERY_GET_USER_OFFICES, QUERY_CUMULATIVE_TAX_NO_EXEMPT, QUERY_CUMULATIVE_TAX_WITH_EXEMPT, + QUERY_INSERT_NOTIFICATION, }; #[derive(Debug, Clone)] @@ -47,6 +51,14 @@ struct Director { may_start_transport: bool, } +#[derive(Debug, Clone)] +struct DirectorResignationCandidate { + id: i32, + employer_user_id: i32, + director_character_id: i32, + satisfaction: i32, +} + #[derive(Debug, Clone)] struct ProductionPlan { falukant_user_id: i32, @@ -91,6 +103,7 @@ struct TransportVehicle { pub struct DirectorWorker { base: BaseWorker, last_run: Option, + last_resignation_check: Option, } // Maximale Anzahl paralleler Produktionen pro Branch @@ -114,6 +127,7 @@ impl DirectorWorker { Self { base: BaseWorker::new("DirectorWorker", pool, broker), last_run: None, + last_resignation_check: None, } } @@ -141,6 +155,84 @@ impl DirectorWorker { self.perform_task()?; self.pay_salary()?; self.calculate_satisfaction()?; + self.process_director_resignations()?; + Ok(()) + } + + fn process_director_resignations(&mut self) -> Result<(), DbError> { + let now = Instant::now(); + let should_run = match self.last_resignation_check { + None => true, + Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(24 * 60 * 60), + }; + if !should_run { + return Ok(()); + } + + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare( + "get_directors_for_resignation_check", + QUERY_GET_DIRECTORS_FOR_RESIGNATION_CHECK, + )?; + conn.prepare("delete_director_by_id", QUERY_DELETE_DIRECTOR_BY_ID)?; + conn.prepare("insert_notification", QUERY_INSERT_NOTIFICATION)?; + + let rows = conn.execute("get_directors_for_resignation_check", &[])?; + let candidates: Vec = rows + .into_iter() + .filter_map(Self::map_row_to_resignation_candidate) + .collect(); + + let mut rng = rand::thread_rng(); + for candidate in candidates { + let sat = candidate.satisfaction.clamp(0, 100) as f64; + let resignation_probability = 1.0 - (sat / 100.0); + if resignation_probability > 0.5 { + let risk_percent = (resignation_probability * 100.0 * 100.0).round() / 100.0; + let payload = format!( + r#"{{"tr":"director.resignation_risk_high","event":"director_resignation_risk_high","director_id":{},"director_character_id":{},"risk_percent":{},"satisfaction":{},"threshold_percent":50}}"#, + candidate.id, + candidate.director_character_id, + risk_percent, + candidate.satisfaction.clamp(0, 100) + ); + let _ = conn.execute("insert_notification", &[&candidate.employer_user_id, &payload]); + } + let roll: f64 = rng.gen_range(0.0..1.0); + if roll >= resignation_probability { + continue; + } + + let deleted = conn.execute("delete_director_by_id", &[&candidate.id])?; + if deleted.is_empty() { + continue; + } + + eprintln!( + "[DirectorWorker] Director {} kündigt (character_id={}, user_id={}, satisfaction={}, prob={:.4}, roll={:.4})", + candidate.id, + candidate.director_character_id, + candidate.employer_user_id, + candidate.satisfaction, + resignation_probability, + roll + ); + + self.base.broker.publish(format!( + r#"{{"event":"directorchanged","user_id":{}}}"#, + candidate.employer_user_id + )); + self.base.broker.publish(format!( + r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, + candidate.employer_user_id + )); + } + + self.last_resignation_check = Some(now); Ok(()) } @@ -198,6 +290,15 @@ impl DirectorWorker { }) } + fn map_row_to_resignation_candidate(row: Row) -> Option { + Some(DirectorResignationCandidate { + id: row.get("id")?.parse().ok()?, + employer_user_id: row.get("employer_user_id")?.parse().ok()?, + director_character_id: row.get("director_character_id")?.parse().ok()?, + satisfaction: row.get("satisfaction")?.parse().ok()?, + }) + } + fn start_productions(&mut self, director: &Director) -> Result<(), DbError> { self.base .set_current_step("DirectorWorker: start_productions"); diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 0758b5d..00e4dad 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -638,6 +638,21 @@ pub const QUERY_GET_DIRECTOR_USER: &str = r#" SELECT fu.id AS falukant_user_id FROM falukant_data.director d JOIN falukant_data.falukant_user fu ON fu.id = d.employer_user_id WHERE d.id = $1 LIMIT 1; "#; +pub const QUERY_GET_DIRECTORS_FOR_RESIGNATION_CHECK: &str = r#" +SELECT d.id, + d.employer_user_id, + d.director_character_id, + COALESCE(d.satisfaction, 0)::int AS satisfaction + FROM falukant_data.director d + WHERE d.employer_user_id IS NOT NULL; +"#; + +pub const QUERY_DELETE_DIRECTOR_BY_ID: &str = r#" +DELETE FROM falukant_data.director + WHERE id = $1::int + RETURNING employer_user_id, director_character_id; +"#; + pub const QUERY_COUNT_VEHICLES_IN_BRANCH_REGION: &str = r#" SELECT COUNT(v.id) AS cnt FROM falukant_data.vehicle v WHERE v.falukant_user_id = $1 AND v.region_id = $2; "#;