From 710a2a62b2d735c33e13d1277fc7a7d7fa1db315 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 28 Jan 2026 14:46:57 +0100 Subject: [PATCH] Refactor SQL query in DirectorWorker: Remove original_sell_cost from selection and enhance cost resolution logic for improved clarity and efficiency in database interactions. --- YpDaemon/src/worker/politics.rs | 1127 ++++++++++++++++++ YpDaemon/src/worker/sql.rs | 1927 +++++++++++++++++++++++++++++++ 2 files changed, 3054 insertions(+) create mode 100644 YpDaemon/src/worker/politics.rs create mode 100644 YpDaemon/src/worker/sql.rs diff --git a/YpDaemon/src/worker/politics.rs b/YpDaemon/src/worker/politics.rs new file mode 100644 index 0000000..3163d1f --- /dev/null +++ b/YpDaemon/src/worker/politics.rs @@ -0,0 +1,1127 @@ +use crate::db::{ConnectionPool, DbError, Row}; +use crate::message_broker::MessageBroker; +use std::collections::HashSet; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use chrono::{Local, Timelike}; + +use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_COUNT_OFFICES_PER_REGION, + QUERY_FIND_OFFICE_GAPS, + QUERY_SELECT_NEEDED_ELECTIONS, + QUERY_INSERT_CANDIDATES, + QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES, + QUERY_PROCESS_EXPIRED_AND_FILL, + QUERY_USERS_IN_CITIES_OF_REGIONS, + QUERY_NOTIFY_OFFICE_EXPIRATION, + QUERY_NOTIFY_ELECTION_CREATED, + QUERY_NOTIFY_OFFICE_FILLED, + QUERY_GET_USERS_WITH_EXPIRING_OFFICES, + QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS, + QUERY_GET_USERS_WITH_FILLED_OFFICES, + QUERY_PROCESS_ELECTIONS, + QUERY_TRIM_EXCESS_OFFICES_GLOBAL, + QUERY_FIND_AVAILABLE_CHURCH_OFFICES, + QUERY_FIND_CHURCH_SUPERVISOR, + QUERY_GET_CHURCH_OFFICE_REQUIREMENTS, + QUERY_GET_PENDING_CHURCH_APPLICATIONS, + QUERY_CHECK_CHARACTER_ELIGIBILITY, + QUERY_APPROVE_CHURCH_APPLICATION, + QUERY_REJECT_CHURCH_APPLICATION, + QUERY_CREATE_CHURCH_APPLICATION_JOB, + QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE, + QUERY_REMOVE_LOWER_RANKED_POLITICAL_OFFICES, + QUERY_REMOVE_LOWER_RANKED_CHURCH_OFFICES, +}; + +pub struct PoliticsWorker { + base: BaseWorker, +} + +#[derive(Debug, Clone)] +struct OfficeCounts { + region_id: i32, + required: i32, + occupied: i32, +} + +#[derive(Debug, Clone)] +struct Election { + election_id: i32, + region_id: i32, + posts_to_fill: i32, +} + +#[derive(Debug, Clone)] +struct OfficeGap { + office_type_id: i32, + region_id: i32, + gaps: i32, +} + +#[derive(Debug, Clone)] +struct Office { + office_id: i32, + office_type_id: i32, + character_id: i32, + region_id: i32, +} + +#[derive(Debug, Clone)] +struct AvailableChurchOffice { + office_type_id: i32, + seats_per_region: i32, + region_id: i32, + occupied_seats: i32, +} + +#[derive(Debug, Clone)] +struct ChurchSupervisor { + supervisor_character_id: i32, +} + +#[derive(Debug, Clone)] +struct ChurchOfficeRequirement { + prerequisite_office_type_id: Option, + min_title_level: Option, +} + +#[derive(Debug, Clone)] +struct ChurchApplication { + application_id: i32, + office_type_id: i32, + applicant_character_id: i32, +} + +// --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------ + + +impl PoliticsWorker { + pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { + Self { + base: BaseWorker::new("PoliticsWorker", pool, broker), + } + } + + fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc) { + let mut last_execution: Option = None; + let mut last_church_office_run: Option = None; + + while state.running_worker.load(Ordering::Relaxed) { + let now = Instant::now(); + let should_run = match last_execution { + None => true, + Some(prev) => now.saturating_duration_since(prev) >= Duration::from_secs(24 * 3600), + }; + + if should_run { + if let Err(err) = Self::perform_daily_politics_task(&pool, &broker) { + eprintln!("[PoliticsWorker] Fehler bei performDailyPoliticsTask: {err}"); + } + last_execution = Some(now); + } + + // Church Office Job um 13 Uhr + if Self::is_time_13_00() { + let should_run_church = match last_church_office_run { + None => true, + Some(prev) => { + // Prüfe ob seit letztem Lauf mindestens 23 Stunden vergangen sind + // (um sicherzustellen, dass es nur einmal pro Tag läuft) + now.saturating_duration_since(prev) >= Duration::from_secs(23 * 3600) + } + }; + + if should_run_church { + if let Err(err) = Self::perform_church_office_task(&pool, &broker) { + eprintln!("[PoliticsWorker] Fehler bei performChurchOfficeTask: {err}"); + } + last_church_office_run = Some(now); + } + } + + // Entspricht ungefähr der 5-Sekunden-Schleife im C++-Code + for _ in 0..5 { + if !state.running_worker.load(Ordering::Relaxed) { + break; + } + std::thread::sleep(Duration::from_secs(1)); + } + } + } + + /// Prüft ob die aktuelle Uhrzeit 13:00 ist (mit Toleranz von ±1 Minute) + fn is_time_13_00() -> bool { + let now = Local::now(); + let hour = now.hour(); + let minute = now.minute(); + hour == 13 && minute <= 1 + } + + fn perform_daily_politics_task( + pool: &ConnectionPool, + broker: &MessageBroker, + ) -> Result<(), DbError> { + // 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur) + Self::evaluate_political_positions(pool)?; + + // 2) Schema-Änderungen abgleichen: neue / zusätzliche Ämter anlegen, + // ohne bestehende Amtsinhaber bei Reduktion zu entfernen. + Self::sync_offices_with_types(pool)?; + + // 3) Ämter, die bald auslaufen, benachrichtigen + Self::notify_office_expirations(pool, broker)?; + + // 4) Abgelaufene Ämter verarbeiten und neue besetzen + let new_offices_direct = Self::process_expired_offices_and_fill(pool)?; + if !new_offices_direct.is_empty() { + // Entferne niedrigerrangige politische Ämter wenn Character mehrere hat + Self::remove_lower_ranked_political_offices(pool)?; + Self::notify_office_filled(pool, broker, &new_offices_direct)?; + } + + // 5) Neue Wahlen planen + let new_elections = Self::schedule_elections(pool)?; + + // 6) Für 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 + // in obiger Liste enthalten. Falls nicht, fügen wir sie + // sicherheitshalber hinzu. + for e in new_elections.iter() { + if !elections_to_fill.iter().any(|x| x.election_id == e.election_id) { + elections_to_fill.push(e.clone()); + } + } + + if !elections_to_fill.is_empty() { + Self::insert_candidates_for_elections(pool, &elections_to_fill)?; + + // Benachrichtige User in betroffenen Regionen + let region_ids: HashSet = + elections_to_fill.iter().map(|e| e.region_id).collect(); + let user_ids = + Self::get_user_ids_in_cities_of_regions(pool, ®ion_ids)?; + 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 !new_offices_from_elections.is_empty() { + // Entferne niedrigerrangige politische Ämter wenn Character mehrere hat + Self::remove_lower_ranked_political_offices(pool)?; + Self::notify_office_filled(pool, broker, &new_offices_from_elections)?; + } + + // 8) Als letzter Schritt sicherstellen, dass es für 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. + Self::trim_excess_offices_global(pool)?; + + Ok(()) + } + + fn evaluate_political_positions( + pool: &ConnectionPool, + ) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "count_offices_per_region", + QUERY_COUNT_OFFICES_PER_REGION, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare count_offices_per_region: {e}")))?; + let rows = conn.execute("count_offices_per_region", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec count_offices_per_region: {e}")))?; + + let mut result = Vec::with_capacity(rows.len()); + for row in rows { + let region_id = parse_i32(&row, "region_id", -1); + let required = parse_i32(&row, "required_count", 0); + let occupied = parse_i32(&row, "occupied_count", 0); + if region_id >= 0 { + let oc = OfficeCounts { + region_id, + required, + occupied, + }; + // Felder aktiv nutzen: einfache Debug-Ausgabe kann später + // durch Logging ersetzt werden. + eprintln!( + "[PoliticsWorker] Region {}: required={}, occupied={}", + oc.region_id, oc.required, oc.occupied + ); + result.push(oc); + } + } + + Ok(result) + } + + /// Entfernt global alle überzähligen Ämter in Relation zur + /// konfigurierten Sitzzahl pro Amtstyp/Region. + fn trim_excess_offices_global(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "trim_excess_offices_global", + QUERY_TRIM_EXCESS_OFFICES_GLOBAL, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare trim_excess_offices_global: {e}")))?; + + if let Err(err) = conn.execute("trim_excess_offices_global", &[]) { + eprintln!( + "[PoliticsWorker] Fehler bei trim_excess_offices_global: {err}" + ); + } + + Ok(()) + } + + /// Gleicht die konfigurierte Anzahl der Ämter (seats_per_region pro + /// Amtstyp/Region) mit den tatsächlich vorhandenen Einträgen in + /// `falukant_data.political_office` ab und legt für fehlende Sitze + /// zusätzliche Wahlen an. + /// + /// Wichtig: Wenn `seats_per_region` gesenkt wurde und damit aktuell + /// mehr Amtsinhaber existieren als Sitze vorgesehen sind, werden + /// **keine** Amtsinhaber entfernt oder Wahlen zum Abbau erzeugt – die + /// entsprechenden Kombinationen tauchen schlicht nicht in der Gaps‑Liste + /// auf. Erst wenn durch natürliches Auslaufen weniger Ämter existieren + /// als vorgesehen, entstehen wieder Gaps. + fn sync_offices_with_types(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("find_office_gaps", QUERY_FIND_OFFICE_GAPS) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare find_office_gaps: {e}")))?; + let rows = conn.execute("find_office_gaps", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec find_office_gaps: {e}")))?; + + if rows.is_empty() { + return Ok(()); + } + + let mut gaps: Vec = Vec::with_capacity(rows.len()); + for row in rows { + let office_type_id = parse_i32(&row, "office_type_id", -1); + let region_id = parse_i32(&row, "region_id", -1); + let gaps_count = parse_i32(&row, "gaps", 0); + if office_type_id >= 0 && region_id >= 0 && gaps_count > 0 { + gaps.push(OfficeGap { + office_type_id, + region_id, + gaps: gaps_count, + }); + } + } + + if gaps.is_empty() { + 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 + // 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). + // + // Hier nutzen wir bewusst eine einfache, wiederholbare Logik im + // Rust‑Code, statt alles in eine riesige DB‑Funktion zu packen. + + // Wir lehnen uns an die Struktur von `schedule_elections` an, + // erzeugen aber unsere eigenen Einträge in `falukant_data.election`. + let insert_sql = r#" + INSERT INTO falukant_data.election + (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) + SELECT + $1::int AS office_type_id, + CURRENT_DATE AS date, + $2::int AS posts_to_fill, + NOW() AS created_at, + NOW() AS updated_at, + $3::int AS region_id + WHERE NOT EXISTS ( + SELECT 1 + FROM falukant_data.election e + WHERE e.office_type_id = $1::int + AND e.region_id = $3::int + AND e.date::date >= CURRENT_DATE + ); + "#; + + conn.prepare("insert_gap_election", insert_sql)?; + + for gap in gaps { + // Sicherheitshalber nur positive Gaps berücksichtigen. + if gap.gaps <= 0 { + continue; + } + + if let Err(err) = conn.execute( + "insert_gap_election", + &[&gap.office_type_id, &gap.gaps, &gap.region_id], + ) { + eprintln!( + "[PoliticsWorker] Fehler beim Anlegen von Gap‑Wahlen \ + (office_type_id={}, region_id={}): {err}", + gap.office_type_id, gap.region_id + ); + } + } + + Ok(()) + } + + fn schedule_elections(pool: &ConnectionPool) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("select_needed_elections", QUERY_SELECT_NEEDED_ELECTIONS) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare select_needed_elections: {e}")))?; + let rows = conn.execute("select_needed_elections", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec select_needed_elections: {e}")))?; + + let mut elections = Vec::with_capacity(rows.len()); + for row in rows { + let election_id = parse_i32(&row, "election_id", -1); + let region_id = parse_i32(&row, "region_id", -1); + let posts_to_fill = parse_i32(&row, "posts_to_fill", 0); + if election_id >= 0 && region_id >= 0 { + elections.push(Election { + election_id, + region_id, + posts_to_fill, + }); + } + } + + Ok(elections) + } + + /// Findet alle existierenden Wahlen (inkl. manuell angelegter), + /// für die noch keine Kandidaten eingetragen wurden. + fn find_elections_needing_candidates(pool: &ConnectionPool) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "select_elections_needing_candidates", + QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare select_elections_needing_candidates: {e}")))?; + let rows = conn.execute("select_elections_needing_candidates", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec select_elections_needing_candidates: {e}")))?; + + let mut elections = Vec::with_capacity(rows.len()); + for row in rows { + let election_id = parse_i32(&row, "election_id", -1); + let region_id = parse_i32(&row, "region_id", -1); + let posts_to_fill = parse_i32(&row, "posts_to_fill", 0); + if election_id >= 0 && region_id >= 0 && posts_to_fill > 0 { + elections.push(Election { + election_id, + region_id, + posts_to_fill, + }); + } + } + + Ok(elections) + } + + fn insert_candidates_for_elections( + pool: &ConnectionPool, + elections: &[Election], + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("insert_candidates", QUERY_INSERT_CANDIDATES) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare insert_candidates: {e}")))?; + + for e in elections { + conn.execute( + "insert_candidates", + &[&e.election_id, &e.region_id, &e.posts_to_fill], + ).map_err(|err| DbError::new(format!("[PoliticsWorker] exec insert_candidates eid={} rid={}: {}", e.election_id, e.region_id, err)))?; + } + + Ok(()) + } + + fn process_expired_offices_and_fill( + pool: &ConnectionPool, + ) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("process_expired_and_fill", QUERY_PROCESS_EXPIRED_AND_FILL) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare process_expired_and_fill: {e}")))?; + let rows = conn.execute("process_expired_and_fill", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec process_expired_and_fill: {e}")))?; + + Ok(rows + .into_iter() + .filter_map(map_row_to_office) + .collect()) + } + + fn get_user_ids_in_cities_of_regions( + pool: &ConnectionPool, + region_ids: &HashSet, + ) -> Result, DbError> { + if region_ids.is_empty() { + return Ok(Vec::new()); + } + + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_users_in_cities", QUERY_USERS_IN_CITIES_OF_REGIONS)?; + + let mut user_ids = Vec::new(); + for rid in region_ids { + let rows = conn.execute("get_users_in_cities", &[rid])?; + for row in rows { + if let Some(uid) = row.get("user_id").and_then(|v| v.parse::().ok()) { + user_ids.push(uid); + } + } + } + + Ok(user_ids) + } + + fn notify_office_expirations( + pool: &ConnectionPool, + broker: &MessageBroker, + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("notify_office_expiration", QUERY_NOTIFY_OFFICE_EXPIRATION) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare notify_office_expiration: {e}")))?; + conn.execute("notify_office_expiration", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec notify_office_expiration: {e}")))?; + + conn.prepare( + "get_users_with_expiring_offices", + QUERY_GET_USERS_WITH_EXPIRING_OFFICES, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare get_users_with_expiring_offices: {e}")))?; + let rows = conn.execute("get_users_with_expiring_offices", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec get_users_with_expiring_offices: {e}")))?; + + for row in rows { + if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::().ok()) { + let msg = + format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + broker.publish(msg); + } + } + + Ok(()) + } + + fn notify_election_created( + pool: &ConnectionPool, + broker: &MessageBroker, + user_ids: &[i32], + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("notify_election_created", QUERY_NOTIFY_ELECTION_CREATED) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare notify_election_created: {e}")))?; + + for uid in user_ids { + conn.execute("notify_election_created", &[uid]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec notify_election_created uid={}: {}", uid, e)))?; + } + + conn.prepare( + "get_users_in_regions_with_elections", + QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare get_users_in_regions_with_elections: {e}")))?; + let rows = conn.execute("get_users_in_regions_with_elections", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec get_users_in_regions_with_elections: {e}")))?; + + for row in rows { + if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::().ok()) { + let msg = + format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + broker.publish(msg); + } + } + + Ok(()) + } + + fn notify_office_filled( + pool: &ConnectionPool, + broker: &MessageBroker, + new_offices: &[Office], + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("notify_office_filled", QUERY_NOTIFY_OFFICE_FILLED) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare notify_office_filled: {e}")))?; + + for office in new_offices { + // Debug-Logging mit allen Feldern, damit sie aktiv genutzt werden + eprintln!( + "[PoliticsWorker] Office filled: id={}, type={}, character={}, region={}", + office.office_id, office.office_type_id, office.character_id, office.region_id + ); + conn.execute("notify_office_filled", &[&office.character_id]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec notify_office_filled office_id={} character_id={}: {}", office.office_id, office.character_id, e)))?; + } + + conn.prepare( + "get_users_with_filled_offices", + QUERY_GET_USERS_WITH_FILLED_OFFICES, + ).map_err(|e| DbError::new(format!("[PoliticsWorker] prepare get_users_with_filled_offices: {e}")))?; + let rows = conn.execute("get_users_with_filled_offices", &[]) + .map_err(|e| DbError::new(format!("[PoliticsWorker] exec get_users_with_filled_offices: {e}")))?; + + for row in rows { + if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::().ok()) { + let msg = + format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + broker.publish(msg); + } + } + + Ok(()) + } + + fn process_elections(pool: &ConnectionPool) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("process_elections", QUERY_PROCESS_ELECTIONS)?; + let rows = conn.execute("process_elections", &[])?; + + Ok(rows + .into_iter() + .filter_map(map_row_to_office) + .collect()) + } + + /// Verarbeitet Church Office Jobs um 13 Uhr: + /// - Findet verfügbare Positionen + /// - Verarbeitet bestehende Bewerbungen + /// - Erstellt neue Bewerbungen falls nötig + fn perform_church_office_task( + pool: &ConnectionPool, + broker: &MessageBroker, + ) -> Result<(), DbError> { + eprintln!("[PoliticsWorker] Starte Church Office Task um 13 Uhr"); + + // 1) Verfügbare Church Office Positionen finden + let available_offices = Self::find_available_church_offices(pool)?; + eprintln!( + "[PoliticsWorker] Gefunden: {} verfügbare Church Office Positionen", + available_offices.len() + ); + + // 2) Für jede verfügbare Position Bewerbungen verarbeiten + for office in &available_offices { + // Supervisor finden + if let Some(supervisor) = Self::find_church_supervisor(pool, office.region_id, office.office_type_id)? { + // Bestehende Bewerbungen für diesen Supervisor verarbeiten + Self::process_church_applications(pool, broker, supervisor.supervisor_character_id)?; + + // Falls noch Plätze frei sind, neue Bewerbungen erstellen + let remaining_seats = office.seats_per_region - office.occupied_seats; + if remaining_seats > 0 { + Self::create_church_application_jobs( + pool, + office.office_type_id, + office.region_id, + supervisor.supervisor_character_id, + remaining_seats, + )?; + } + } else { + eprintln!( + "[PoliticsWorker] Kein Supervisor gefunden für office_type_id={}, region_id={}", + office.office_type_id, office.region_id + ); + } + } + + eprintln!("[PoliticsWorker] Church Office Task abgeschlossen"); + Ok(()) + } + + fn find_available_church_offices( + pool: &ConnectionPool, + ) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "find_available_church_offices", + QUERY_FIND_AVAILABLE_CHURCH_OFFICES, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare find_available_church_offices: {e}" + )) + })?; + let rows = conn + .execute("find_available_church_offices", &[]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec find_available_church_offices: {e}" + )) + })?; + + let mut offices = Vec::new(); + for row in rows { + let office_type_id = parse_i32(&row, "office_type_id", -1); + let region_id = parse_i32(&row, "region_id", -1); + let seats_per_region = parse_i32(&row, "seats_per_region", 0); + let occupied_seats = parse_i32(&row, "occupied_seats", 0); + + if office_type_id >= 0 && region_id >= 0 { + offices.push(AvailableChurchOffice { + office_type_id, + seats_per_region, + region_id, + occupied_seats, + }); + } + } + + Ok(offices) + } + + fn find_church_supervisor( + pool: &ConnectionPool, + region_id: i32, + office_type_id: i32, + ) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("find_church_supervisor", QUERY_FIND_CHURCH_SUPERVISOR) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare find_church_supervisor: {e}" + )) + })?; + let rows = conn + .execute("find_church_supervisor", &[®ion_id, &office_type_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec find_church_supervisor: {e}" + )) + })?; + + for row in rows { + let supervisor_character_id = parse_i32(&row, "supervisor_character_id", -1); + + if supervisor_character_id >= 0 { + return Ok(Some(ChurchSupervisor { + supervisor_character_id, + })); + } + } + + Ok(None) + } + + fn process_church_applications( + pool: &ConnectionPool, + broker: &MessageBroker, + supervisor_id: i32, + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + // Bewerbungen für diesen Supervisor abrufen + conn.prepare( + "get_pending_church_applications", + QUERY_GET_PENDING_CHURCH_APPLICATIONS, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare get_pending_church_applications: {e}" + )) + })?; + let rows = conn + .execute("get_pending_church_applications", &[&supervisor_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec get_pending_church_applications: {e}" + )) + })?; + + let mut applications = Vec::new(); + for row in rows { + let application_id = parse_i32(&row, "application_id", -1); + let office_type_id = parse_i32(&row, "office_type_id", -1); + let applicant_character_id = parse_i32(&row, "applicant_character_id", -1); + + if application_id >= 0 { + applications.push(ChurchApplication { + application_id, + office_type_id, + applicant_character_id, + }); + } + } + + // Voraussetzungen prüfen und Bewerbungen verarbeiten + conn.prepare( + "get_church_office_requirements", + QUERY_GET_CHURCH_OFFICE_REQUIREMENTS, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare get_church_office_requirements: {e}" + )) + })?; + + conn.prepare( + "check_character_eligibility", + QUERY_CHECK_CHARACTER_ELIGIBILITY, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare check_character_eligibility: {e}" + )) + })?; + + conn.prepare("approve_church_application", QUERY_APPROVE_CHURCH_APPLICATION) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare approve_church_application: {e}" + )) + })?; + + conn.prepare("reject_church_application", QUERY_REJECT_CHURCH_APPLICATION) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare reject_church_application: {e}" + )) + })?; + + for app in &applications { + // Voraussetzungen für dieses Amt abrufen + let req_rows = conn + .execute("get_church_office_requirements", &[&app.office_type_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec get_church_office_requirements: {e}" + )) + })?; + + let mut requirements = Vec::new(); + for req_row in req_rows { + let prerequisite_office_type_id = req_row + .get("prerequisite_office_type_id") + .and_then(|v| v.parse::().ok()); + let min_title_level = req_row + .get("min_title_level") + .and_then(|v| v.parse::().ok()); + + requirements.push(ChurchOfficeRequirement { + prerequisite_office_type_id, + min_title_level, + }); + } + + // Prüfe ob Character die Voraussetzungen erfüllt + let mut eligible = true; + for req in &requirements { + let elig_rows = conn + .execute( + "check_character_eligibility", + &[ + &app.applicant_character_id, + &req.prerequisite_office_type_id, + &req.min_title_level, + ], + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec check_character_eligibility: {e}" + )) + })?; + + for elig_row in elig_rows { + let has_prerequisite: bool = elig_row + .get("has_prerequisite") + .and_then(|v| v.parse::().ok()) + .unwrap_or(false); + let meets_title_requirement: bool = elig_row + .get("meets_title_requirement") + .and_then(|v| v.parse::().ok()) + .unwrap_or(false); + + if !has_prerequisite || !meets_title_requirement { + eligible = false; + break; + } + } + } + + // Bewerbung genehmigen oder ablehnen + if eligible { + let approve_rows = conn + .execute("approve_church_application", &[&app.application_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec approve_church_application: {e}" + )) + })?; + + if !approve_rows.is_empty() { + eprintln!( + "[PoliticsWorker] Church Application {} genehmigt (office_type_id={}, character_id={})", + app.application_id, app.office_type_id, app.applicant_character_id + ); + + // Entferne niedrigerrangige Church Offices wenn Character mehrere hat + Self::remove_lower_ranked_church_offices(pool)?; + + // Benachrichtigung senden + if let Some(user_id) = Self::get_user_id_for_character(pool, app.applicant_character_id)? { + let msg = format!( + r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, + user_id + ); + broker.publish(msg); + } + } + } else { + conn.execute("reject_church_application", &[&app.application_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec reject_church_application: {e}" + )) + })?; + eprintln!( + "[PoliticsWorker] Church Application {} abgelehnt (Voraussetzungen nicht erfüllt)", + app.application_id + ); + } + } + + Ok(()) + } + + fn create_church_application_jobs( + pool: &ConnectionPool, + office_type_id: i32, + region_id: i32, + supervisor_id: i32, + count: i32, + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + // Charaktere für diese Region finden + conn.prepare( + "get_characters_for_church_office", + QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare get_characters_for_church_office: {e}" + )) + })?; + + let rows = conn + .execute("get_characters_for_church_office", &[®ion_id, &count]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec get_characters_for_church_office: {e}" + )) + })?; + + conn.prepare( + "create_church_application_job", + QUERY_CREATE_CHURCH_APPLICATION_JOB, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare create_church_application_job: {e}" + )) + })?; + + let mut created = 0; + for row in rows { + if let Some(character_id) = row + .get("character_id") + .and_then(|v| v.parse::().ok()) + { + let app_rows = conn + .execute( + "create_church_application_job", + &[&office_type_id, &character_id, ®ion_id, &supervisor_id], + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec create_church_application_job: {e}" + )) + })?; + + if !app_rows.is_empty() { + created += 1; + eprintln!( + "[PoliticsWorker] Church Application Job erstellt: office_type_id={}, character_id={}, region_id={}", + office_type_id, character_id, region_id + ); + } + } + } + + eprintln!( + "[PoliticsWorker] {} Church Application Jobs erstellt für office_type_id={}, region_id={}", + created, office_type_id, region_id + ); + + Ok(()) + } + + fn get_user_id_for_character( + pool: &ConnectionPool, + character_id: i32, + ) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + let query = "SELECT user_id FROM falukant_data.character WHERE id = $1"; + conn.prepare("get_user_id_for_character", query) + .map_err(|e| DbError::new(format!("[PoliticsWorker] prepare get_user_id_for_character: {e}")))?; + let rows = conn + .execute("get_user_id_for_character", &[&character_id]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec get_user_id_for_character: {e}" + )) + })?; + + for row in rows { + if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::().ok()) { + return Ok(Some(user_id)); + } + } + + Ok(None) + } + + /// Entfernt niedrigerrangige politische Ämter wenn ein Character mehrere hat + fn remove_lower_ranked_political_offices(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "remove_lower_ranked_political_offices", + QUERY_REMOVE_LOWER_RANKED_POLITICAL_OFFICES, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare remove_lower_ranked_political_offices: {e}" + )) + })?; + + conn.execute("remove_lower_ranked_political_offices", &[]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec remove_lower_ranked_political_offices: {e}" + )) + })?; + + Ok(()) + } + + /// Entfernt niedrigerrangige Church Offices wenn ein Character mehrere hat + fn remove_lower_ranked_church_offices(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "remove_lower_ranked_church_offices", + QUERY_REMOVE_LOWER_RANKED_CHURCH_OFFICES, + ) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] prepare remove_lower_ranked_church_offices: {e}" + )) + })?; + + conn.execute("remove_lower_ranked_church_offices", &[]) + .map_err(|e| { + DbError::new(format!( + "[PoliticsWorker] exec remove_lower_ranked_church_offices: {e}" + )) + })?; + + Ok(()) + } +} + +impl Worker for PoliticsWorker { + fn start_worker_thread(&mut self) { + let pool = self.base.pool.clone(); + let broker = self.base.broker.clone(); + + self.base + .start_worker_with_loop(move |state: Arc| { + PoliticsWorker::run_loop(pool.clone(), broker.clone(), state); + }); + } + + fn stop_worker_thread(&mut self) { + self.base.stop_worker(); + } + + fn enable_watchdog(&mut self) { + self.base.start_watchdog(); + } +} + +fn parse_i32(row: &Row, key: &str, default: i32) -> i32 { + row.get(key) + .and_then(|v| v.parse::().ok()) + .unwrap_or(default) +} + +fn map_row_to_office(row: Row) -> Option { + Some(Office { + office_id: row.get("office_id")?.parse().ok()?, + office_type_id: row.get("office_type_id")?.parse().ok()?, + character_id: row.get("character_id")?.parse().ok()?, + region_id: row.get("region_id")?.parse().ok()?, + }) +} + + diff --git a/YpDaemon/src/worker/sql.rs b/YpDaemon/src/worker/sql.rs new file mode 100644 index 0000000..3a7a953 --- /dev/null +++ b/YpDaemon/src/worker/sql.rs @@ -0,0 +1,1927 @@ +// Centralized SQL strings for workers. + +pub const QUERY_UPDATE_MONEY: &str = r#" +SELECT falukant_data.update_money($1, $2, $3); +"#; + +pub const QUERY_GET_MONEY: &str = r#" +SELECT money FROM falukant_data.falukant_user WHERE id = $1; +"#; + + +pub const QUERY_GET_RANDOM_USER: &str = r#" +SELECT id FROM falukant_data.falukant_user ORDER BY RANDOM() LIMIT 1; +"#; + +pub const QUERY_GET_RANDOM_INFANT: &str = r#" +SELECT c.id AS character_id, c.user_id, CURRENT_DATE - c.birthdate::date AS age_days +FROM falukant_data."character" c +WHERE c.user_id IS NOT NULL AND c.health > 0 AND CURRENT_DATE - c.birthdate::date <= 730 +ORDER BY RANDOM() LIMIT 1; +"#; + +pub const QUERY_GET_RANDOM_CITY: &str = r#" +SELECT r.id AS region_id FROM falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id WHERE tr.label_tr = 'city' ORDER BY RANDOM() LIMIT 1; +"#; + +pub const QUERY_GET_AFFECTED_USERS: &str = r#" +SELECT DISTINCT b.falukant_user_id AS user_id FROM falukant_data.branch b WHERE b.region_id = $1 AND b.falukant_user_id IS NOT NULL; +"#; + +pub const QUERY_UPDATE_WEATHER: &str = r#" +WITH all_regions AS ( + SELECT DISTINCT r.id AS region_id FROM falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id WHERE tr.label_tr = 'city' +) +INSERT INTO falukant_data.weather (region_id, weather_type_id) +SELECT ar.region_id, (SELECT wt.id FROM falukant_type.weather wt ORDER BY random() + ar.region_id * 0 LIMIT 1) FROM all_regions ar +ON CONFLICT (region_id) DO UPDATE SET weather_type_id = EXCLUDED.weather_type_id; +"#; + +pub const QUERY_INSERT_NOTIFICATION: &str = r#" +INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) +VALUES ($1, $2, FALSE, NOW(), NOW()); +"#; + +// Product pricing +pub const QUERY_GET_PRODUCT_COST: &str = r#" +SELECT sell_cost FROM falukant_type.product WHERE id = $1; +"#; + +pub const QUERY_GET_DIRECTORS: &str = r#" +SELECT d.may_produce, d.may_sell, d.may_start_transport, b.id AS branch_id, fu.id AS falukantUserId, d.id +FROM falukant_data.director d +JOIN falukant_data.falukant_user fu ON fu.id = d.employer_user_id +JOIN falukant_data.character c ON c.id = d.director_character_id +JOIN falukant_data.branch b ON b.region_id = c.region_id AND b.falukant_user_id = fu.id +WHERE current_time BETWEEN '08:00:00' AND '17:00:00'; +"#; + +pub const QUERY_GET_BEST_PRODUCTION: &str = r#" +SELECT fdu.id falukant_user_id, CAST(fdu.money AS text) AS money, fdu.certificate, ftp.id product_id, ftp.label_tr, fdb.region_id, +(SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = fdb.id) AS stock_size, +COALESCE((SELECT SUM(COALESCE(fdi.quantity, 0)) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = fdb.id), 0) AS used_in_stock, +(ftp.sell_cost * (fdtpw.worth_percent + (fdk_character.knowledge * 2 + fdk_director.knowledge) / 3) / 100 - 6 * ftp.category) / (300.0 * ftp.production_time) AS worth, +fdb.id AS branch_id, (SELECT COUNT(id) FROM falukant_data.production WHERE branch_id = fdb.id) AS running_productions, +COALESCE((SELECT SUM(COALESCE(fdp.quantity, 0)) quantity FROM falukant_data.production fdp WHERE fdp.branch_id = fdb.id), 0) AS running_productions_quantity +FROM falukant_data.director fdd +JOIN falukant_data.character fdc ON fdc.id = fdd.director_character_id +JOIN falukant_data.falukant_user fdu ON fdd.employer_user_id = fdu.id +JOIN falukant_data.character user_character ON user_character.user_id = fdu.id +JOIN falukant_data.branch fdb ON fdb.falukant_user_id = fdu.id AND fdb.region_id = fdc.region_id +JOIN falukant_data.town_product_worth fdtpw ON fdtpw.region_id = fdb.region_id +JOIN falukant_data.knowledge fdk_character ON fdk_character.product_id = fdtpw.product_id AND fdk_character.character_id = user_character.id +JOIN falukant_data.knowledge fdk_director ON fdk_director.product_id = fdtpw.product_id AND fdk_director.character_id = fdd.director_character_id +JOIN falukant_type.product ftp ON ftp.id = fdtpw.product_id AND ftp.category <= fdu.certificate +WHERE fdd.id = $1 AND fdb.id = $2 ORDER BY worth DESC LIMIT 1; +"#; + +pub const QUERY_INSERT_PRODUCTION: &str = r#" +INSERT INTO falukant_data.production (branch_id, product_id, quantity, weather_type_id) VALUES ($1, $2, $3, (SELECT weather_type_id FROM falukant_data.weather WHERE region_id = $4)); +"#; + +// Character creation related queries (missing from earlier extraction) +pub const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#" + SELECT created_at + FROM falukant_data."character" + WHERE user_id IS NULL + AND created_at::date = CURRENT_DATE + ORDER BY created_at DESC + LIMIT 1; +"#; + +pub const QUERY_GET_TOWN_REGION_IDS: &str = r#" + SELECT fdr.id + FROM falukant_data.region fdr + JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id + WHERE ftr.label_tr = 'city'; +"#; + +pub const QUERY_LOAD_FIRST_NAMES: &str = r#" + SELECT id, gender + FROM falukant_predefine.firstname; +"#; + +pub const QUERY_LOAD_LAST_NAMES: &str = r#" + SELECT id + FROM falukant_predefine.lastname; +"#; + +pub const QUERY_INSERT_CHARACTER: &str = r#" + INSERT INTO falukant_data.character( + user_id, + region_id, + first_name, + last_name, + birthdate, + gender, + created_at, + updated_at, + title_of_nobility + ) VALUES ( + NULL, + $1, + $2, + $3, + NOW(), + $4, + NOW(), + NOW(), + $5 + ); +"#; + +pub const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#" + WITH aged AS ( + SELECT + c.id, + (current_date - c.birthdate::date) AS age, + c.user_id + FROM + falukant_data.character c + WHERE + c.user_id IS NULL + AND (current_date - c.birthdate::date) > 60 + ), + always_sel AS ( + SELECT * + FROM aged + WHERE age > 85 + ), + random_sel AS ( + SELECT * + FROM aged + WHERE age <= 85 + ORDER BY random() + LIMIT 10 + ) + SELECT * + FROM always_sel + UNION ALL + SELECT * + FROM random_sel; +"#; + +pub const QUERY_MARK_CHARACTER_DECEASED: &str = r#" + DELETE FROM falukant_data.character + WHERE id = $1; +"#; + +pub const QUERY_GET_BRANCH_CAPACITY: &str = r#" +SELECT (SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = $1) AS stock_size, +COALESCE((SELECT SUM(COALESCE(fdi.quantity, 0)) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = $1), 0) AS used_in_stock, +(SELECT COUNT(id) FROM falukant_data.production WHERE branch_id = $1) AS running_productions, +COALESCE((SELECT SUM(COALESCE(fdp.quantity, 0)) quantity FROM falukant_data.production fdp WHERE fdp.branch_id = $1), 0) AS running_productions_quantity; +"#; + +pub const QUERY_GET_INVENTORY: &str = r#" +SELECT i.id, i.product_id, i.quantity, i.quality, p.sell_cost, fu.id AS user_id, b.region_id, b.id AS branch_id, COALESCE(tpw.worth_percent, 100.0) AS worth_percent +FROM falukant_data.inventory i +JOIN falukant_data.stock s ON s.id = i.stock_id +JOIN falukant_data.branch b ON b.id = s.branch_id +JOIN falukant_data.falukant_user fu ON fu.id = b.falukant_user_id +JOIN falukant_data.director d ON d.employer_user_id = fu.id +JOIN falukant_type.product p ON p.id = i.product_id +LEFT JOIN falukant_data.town_product_worth tpw ON tpw.region_id = b.region_id AND tpw.product_id = i.product_id +WHERE d.id = $1 AND b.id = $2; +"#; + +pub const QUERY_REMOVE_INVENTORY: &str = r#" +DELETE FROM falukant_data.inventory WHERE id = $1; +"#; + +pub const QUERY_ADD_SELL_LOG: &str = r#" +INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id) VALUES ($1, $2, $3, $4) +ON CONFLICT (region_id, product_id, seller_id) DO UPDATE SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity; +"#; +pub const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#" +SELECT + t.id, + t.product_id, + t.size, + t.vehicle_id, + t.source_region_id, + t.target_region_id, + b_target.id AS target_branch_id, + b_source.id AS source_branch_id, + rd.distance AS distance, + v.falukant_user_id AS user_id +FROM falukant_data.transport AS t +JOIN falukant_data.vehicle AS v ON v.id = t.vehicle_id +JOIN falukant_type.vehicle AS vt ON vt.id = v.vehicle_type_id +JOIN falukant_data.region_distance AS rd ON ((rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) +LEFT JOIN falukant_data.branch AS b_target ON b_target.region_id = t.target_region_id AND b_target.falukant_user_id = v.falukant_user_id +LEFT JOIN falukant_data.branch AS b_source ON b_source.region_id = t.source_region_id AND b_source.falukant_user_id = v.falukant_user_id +WHERE vt.speed > 0 AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' <= NOW(); +"#; + +pub const QUERY_GET_AVAILABLE_STOCKS: &str = r#" +SELECT + stock.id, + stock.quantity AS total_capacity, + ( + SELECT COALESCE(SUM(inventory.quantity), 0) + FROM falukant_data.inventory + WHERE inventory.stock_id = stock.id + ) AS filled +FROM falukant_data.stock stock +JOIN falukant_data.branch branch + ON stock.branch_id = branch.id +WHERE branch.id = $1 +ORDER BY total_capacity DESC; +"#; + +pub const QUERY_INSERT_INVENTORY: &str = r#" +INSERT INTO falukant_data.inventory (stock_id, product_id, quantity, quality, produced_at) +VALUES ($1, $2, $3, $4, NOW()) +RETURNING id; +"#; + +pub const QUERY_UPDATE_INVENTORY_BY_STOCK_PRODUCT: &str = r#" +UPDATE falukant_data.inventory + SET quantity = quantity + $3, + quality = LEAST( + 100, + ROUND( + ((quantity * quality) + ($3 * $4))::numeric / NULLIF(quantity + $3, 0) + ) + ) + WHERE stock_id = $1 AND product_id = $2 + RETURNING id; +"#; + +pub const QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT: &str = r#" +UPDATE falukant_data.vehicle SET region_id = $2, condition = GREATEST(0, condition - $3::int), available_from = NOW(), updated_at = NOW() WHERE id = $1; +"#; + +pub const QUERY_DELETE_TRANSPORT: &str = r#" +DELETE FROM falukant_data.transport WHERE id = $1; +"#; + +#[allow(dead_code)] +pub const QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION: &str = r#" +INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) +VALUES ((SELECT c.user_id FROM falukant_data.character c WHERE c.user_id = $1 LIMIT 1), $2, FALSE, NOW(), NOW()); +"#; + +pub const QUERY_UPDATE_TRANSPORT_SIZE: &str = r#" +UPDATE falukant_data.transport + SET size = $2, + updated_at = NOW() + WHERE id = $1; +"#; + +pub const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#" +SELECT tpw.region_id, tpw.product_id, tpw.worth_percent FROM falukant_data.town_product_worth tpw JOIN falukant_data.branch b ON b.region_id = tpw.region_id WHERE b.falukant_user_id = $1 AND tpw.product_id = $2; +"#; + +// Political offices and cumulative tax +pub const QUERY_GET_USER_OFFICES: &str = r#" +SELECT po.id AS office_id, pot.name AS office_name, po.region_id, rt.label_tr AS region_type +FROM falukant_data.political_office po +JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id +JOIN falukant_data.region r ON r.id = po.region_id +JOIN falukant_type.region rt ON rt.id = r.region_type_id +JOIN falukant_data.character ch ON ch.id = po.character_id +WHERE ch.user_id = $1 + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW(); +"#; + +pub const QUERY_CUMULATIVE_TAX_NO_EXEMPT: &str = r#" +WITH RECURSIVE ancestors AS ( + SELECT id, parent_id, COALESCE(tax_percent,0.0) AS tax_percent FROM falukant_data.region WHERE id = $1 + UNION ALL + SELECT r.id, r.parent_id, COALESCE(r.tax_percent,0.0) FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id +) +SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; +"#; + +pub const QUERY_CUMULATIVE_TAX_WITH_EXEMPT: &str = r#" +WITH RECURSIVE ancestors AS ( + SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END AS tax_percent + FROM falukant_data.region r JOIN falukant_type.region rt ON rt.id = r.region_type_id WHERE r.id = $1 + UNION ALL + SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END + FROM falukant_data.region r JOIN falukant_type.region rt ON rt.id = r.region_type_id JOIN ancestors a ON r.id = a.parent_id +) +SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; +"#; + +pub const QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE: &str = r#" +SELECT v.id AS vehicle_id, vt.capacity AS capacity +FROM falukant_data.vehicle v +JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id +JOIN falukant_data.region_distance rd ON ((rd.source_region_id = v.region_id AND rd.target_region_id = $3) OR (rd.source_region_id = $3 AND rd.target_region_id = v.region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) +WHERE v.falukant_user_id = $1 AND v.region_id = $2; +"#; + +pub const QUERY_INSERT_TRANSPORT: &str = r#" +INSERT INTO falukant_data.transport (source_region_id, target_region_id, product_id, size, vehicle_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()); +"#; + +pub const QUERY_INSERT_EMPTY_TRANSPORT: &str = r#" +INSERT INTO falukant_data.transport (source_region_id, target_region_id, product_id, size, vehicle_id, created_at, updated_at) VALUES ($1, $2, NULL, 0, $3, NOW(), NOW()); +"#; + +pub const QUERY_GET_USER_BRANCHES: &str = r#" +SELECT DISTINCT b.region_id, b.id AS branch_id FROM falukant_data.branch b WHERE b.falukant_user_id = $1 AND b.region_id != $2; +"#; + +pub const QUERY_GET_FREE_VEHICLES_IN_REGION: &str = r#" +SELECT v.id AS vehicle_id, vt.capacity AS capacity FROM falukant_data.vehicle v JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id WHERE v.falukant_user_id = $1 AND v.region_id = $2 AND v.id NOT IN (SELECT DISTINCT t.vehicle_id FROM falukant_data.transport t WHERE t.vehicle_id IS NOT NULL); +"#; + +pub const QUERY_GET_SALARY_TO_PAY: &str = r#" +SELECT d.id, d.employer_user_id, d.income FROM falukant_data.director d WHERE DATE(d.last_salary_payout) < DATE(NOW()); +"#; + +pub const QUERY_SET_SALARY_PAYED: &str = r#" +UPDATE falukant_data.director SET last_salary_payout = NOW() WHERE id = $1; +"#; + +pub const QUERY_UPDATE_SATISFACTION: &str = r#" +WITH new_sats AS ( + SELECT d.id, ROUND(d.income::numeric / (c.title_of_nobility * POWER(1.231, AVG(k.knowledge) / 1.5)) * 100) AS new_satisfaction + FROM falukant_data.director d + JOIN falukant_data.knowledge k ON d.director_character_id = k.character_id + JOIN falukant_data.character c ON c.id = d.director_character_id + GROUP BY d.id, c.title_of_nobility, d.income +) +UPDATE falukant_data.director dir SET satisfaction = ns.new_satisfaction FROM new_sats ns WHERE dir.id = ns.id AND dir.satisfaction IS DISTINCT FROM ns.new_satisfaction RETURNING dir.employer_user_id; +"#; + +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_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; +"#; + +pub const QUERY_COUNT_VEHICLES_IN_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; +"#; + +pub const QUERY_CHECK_ROUTE: &str = r#" +SELECT 1 FROM falukant_data.region_distance rd WHERE (rd.source_region_id = $1 AND rd.target_region_id = $2) OR (rd.source_region_id = $2 AND rd.target_region_id = $1) LIMIT 1; +"#; + +pub const QUERY_GET_BRANCH_REGION: &str = r#" +SELECT region_id FROM falukant_data.branch WHERE id = $1 LIMIT 1; +"#; + +pub const QUERY_GET_AVERAGE_WORTH: &str = r#" +SELECT AVG(tpw.worth_percent) AS avg_worth FROM falukant_data.town_product_worth tpw WHERE tpw.product_id = $1 AND tpw.region_id IN (SELECT region_id FROM falukant_data.branch WHERE falukant_user_id = $2); +"#; + +pub const QUERY_UPDATE_INVENTORY_QTY: &str = r#" +UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; +"#; + +pub const QUERY_GET_USER_STOCKS: &str = r#" +SELECT s.id AS stock_id, s.quantity AS current_capacity +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.falukant_user_id = $1; +"#; + +pub const QUERY_UPDATE_STOCK_CAPACITY: &str = r#" +UPDATE falukant_data.stock + SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) + WHERE id = $2; +"#; + +pub const QUERY_GET_REGION_STOCKS: &str = r#" +SELECT s.id AS stock_id, s.quantity AS current_capacity +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.region_id = $1; +"#; + +pub const QUERY_UPDATE_STOCK_CAPACITY_REGIONAL: &str = r#" +UPDATE falukant_data.stock + SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) + WHERE id = $2; +"#; + +// Stockage manager specific queries +pub const QUERY_GET_TOWNS: &str = r#" + SELECT fdr.id + FROM falukant_data.region fdr + JOIN falukant_type.region ftr + ON ftr.id = fdr.region_type_id + WHERE ftr.label_tr = 'city'; +"#; + +pub const QUERY_INSERT_STOCK: &str = r#" + INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity) + SELECT + $1 AS region_id, + s.id AS stock_type_id, + GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity + FROM falukant_data.branch AS br + CROSS JOIN falukant_type.stock AS s + WHERE br.region_id = $1 + GROUP BY s.id + ORDER BY RANDOM() + LIMIT GREATEST( + ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)), + 1 + ); +"#; + +pub const QUERY_CLEANUP_STOCK: &str = r#" + DELETE FROM falukant_data.buyable_stock + WHERE quantity <= 0; +"#; + +pub const QUERY_GET_REGION_USERS: &str = r#" + SELECT c.user_id + FROM falukant_data.character c + WHERE c.region_id = $1 + AND c.user_id IS NOT NULL; +"#; + +pub const QUERY_GET_REGION_HOUSES: &str = r#" +SELECT uh.id AS house_id, uh.roof_condition, uh.floor_condition, uh.wall_condition, uh.window_condition +FROM falukant_data.user_house uh +JOIN falukant_data.character c ON c.user_id = uh.user_id +WHERE c.region_id = $1 + AND uh.house_type_id NOT IN ( + SELECT id FROM falukant_type.house h WHERE h.label_tr = 'under_bridge' + ); +"#; + +// House worker queries +pub const QUERY_GET_NEW_HOUSE_DATA: &str = r#" + SELECT + h.id AS house_id + FROM + falukant_type.house AS h + WHERE + random() < 0.0001 + AND label_tr <> 'under_bridge'; +"#; + +pub const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#" + INSERT INTO falukant_data.buyable_house (house_type_id) + VALUES ($1); +"#; + +pub const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#" + UPDATE falukant_data.buyable_house + SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), + floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), + wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), + window_condition = ROUND(window_condition - random() * (3 + 0 * id)); +"#; + +pub const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#" + UPDATE falukant_data.user_house + SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), + floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), + wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), + window_condition = ROUND(window_condition - random() * (3 + 0 * id)) + WHERE house_type_id NOT IN ( + SELECT id + FROM falukant_type.house h + WHERE h.label_tr = 'under_bridge' + ); +"#; + +pub const QUERY_UPDATE_HOUSE_QUALITY: &str = r#" +UPDATE falukant_data.user_house + SET roof_condition = GREATEST(0, LEAST(100, roof_condition + $1)), + floor_condition = GREATEST(0, LEAST(100, floor_condition + $1)), + wall_condition = GREATEST(0, LEAST(100, wall_condition + $1)), + window_condition = GREATEST(0, LEAST(100, window_condition + $1)) + WHERE id = $2; +"#; + +pub const QUERY_CHANGE_WEATHER: &str = r#" +UPDATE falukant_data.weather +SET weather_type_id = ( + SELECT id FROM falukant_type.weather ORDER BY RANDOM() LIMIT 1 +) +WHERE region_id = $1; +"#; + +pub const QUERY_GET_RANDOM_CHARACTER: &str = r#" +SELECT id, health +FROM falukant_data."character" +WHERE user_id = $1 AND health > 0 +ORDER BY RANDOM() LIMIT 1; +"#; + +pub const QUERY_UPDATE_HEALTH: &str = r#" +UPDATE falukant_data."character" SET health = $1 WHERE id = $2; +"#; + +pub const QUERY_GET_REGION_CHARACTERS: &str = r#" +SELECT id, health FROM falukant_data."character" WHERE region_id = $1 AND health > 0; +"#; + +pub const QUERY_DELETE_DIRECTOR: &str = r#" +DELETE FROM falukant_data.director WHERE director_character_id = $1 RETURNING employer_user_id; +"#; + +pub const QUERY_DELETE_RELATIONSHIP: &str = r#" +WITH deleted AS ( + DELETE FROM falukant_data.relationship + WHERE character1_id = $1 OR character2_id = $1 + RETURNING CASE WHEN character1_id = $1 THEN character2_id ELSE character1_id END AS related_character_id, relationship_type_id +) +SELECT c.user_id AS related_user_id FROM deleted d JOIN falukant_data.character c ON c.id = d.related_character_id; +"#; + +pub const QUERY_GET_USER_ID: &str = r#" +SELECT user_id FROM falukant_data.character WHERE id = $1; +"#; + +pub const QUERY_DELETE_CHILD_RELATION: &str = r#" +WITH deleted AS ( + DELETE FROM falukant_data.child_relation WHERE child_character_id = $1 RETURNING father_character_id, mother_character_id +) +SELECT cf.user_id AS father_user_id, cm.user_id AS mother_user_id FROM deleted d JOIN falukant_data.character cf ON cf.id = d.father_character_id JOIN falukant_data.character cm ON cm.id = d.mother_character_id; +"#; + +pub const QUERY_DELETE_CHARACTER: &str = r#" +DELETE FROM falukant_data.character WHERE id = $1; +"#; + +pub const QUERY_GET_HEIR: &str = r#" +SELECT child_character_id FROM falukant_data.child_relation WHERE father_character_id = $1 OR mother_character_id = $1 ORDER BY (is_heir IS TRUE) DESC, updated_at DESC LIMIT 1; +"#; + +pub const QUERY_SET_CHARACTER_USER: &str = r#" +UPDATE falukant_data.character SET user_id = $1, updated_at = NOW() WHERE id = $2; +"#; + +pub const QUERY_GET_CURRENT_MONEY: &str = r#" +SELECT money FROM falukant_data.falukant_user WHERE id = $1; +"#; + +pub const QUERY_GET_HOUSE_VALUE: &str = r#" +SELECT COALESCE(SUM(h.cost), 0) AS sum FROM falukant_data.user_house AS uh JOIN falukant_type.house AS h ON uh.house_type_id = h.id WHERE uh.user_id = $1; +"#; + +pub const QUERY_GET_SETTLEMENT_VALUE: &str = r#" +SELECT COALESCE(SUM(b.base_cost), 0) AS sum FROM falukant_data.branch AS br JOIN falukant_type.branch AS b ON br.branch_type_id = b.id WHERE br.falukant_user_id = $1; +"#; + +pub const QUERY_GET_INVENTORY_VALUE: &str = r#" +SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum FROM falukant_data.inventory AS i JOIN falukant_type.product AS p ON i.product_id = p.id JOIN falukant_data.stock AS s ON i.stock_id = s.id JOIN falukant_data.branch AS br ON s.branch_id = br.id WHERE br.falukant_user_id = $1; +"#; + +pub const QUERY_GET_CREDIT_DEBT: &str = r#" +SELECT COALESCE(SUM(remaining_amount), 0) AS sum FROM falukant_data.credit WHERE falukant_user_id = $1; +"#; + +pub const QUERY_COUNT_CHILDREN: &str = r#" +SELECT COUNT(*) AS cnt FROM falukant_data.child_relation WHERE (father_character_id = $1 OR mother_character_id = $1) AND child_character_id != $2; +"#; + +// user_character worker queries +pub const QUERY_GET_USERS_TO_UPDATE: &str = r#" + SELECT id, CURRENT_DATE - birthdate::date AS age, health + FROM falukant_data."character" + WHERE user_id IS NOT NULL; +"#; + +// politics worker queries +pub const QUERY_COUNT_OFFICES_PER_REGION: &str = r#" + WITH + seats_per_region AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + occupied AS ( + SELECT + po.office_type_id, + po.region_id, + COUNT(*) AS occupied_count + FROM falukant_data.political_office AS po + GROUP BY po.office_type_id, po.region_id + ), + combined AS ( + SELECT + spr.region_id, + spr.seats_total AS required_count, + COALESCE(o.occupied_count, 0) AS occupied_count + FROM seats_per_region AS spr + LEFT JOIN occupied AS o + ON spr.office_type_id = o.office_type_id + AND spr.region_id = o.region_id + ) + SELECT + region_id, + SUM(required_count) AS required_count, + SUM(occupied_count) AS occupied_count + FROM combined + GROUP BY region_id; +"#; + +pub const QUERY_FIND_OFFICE_GAPS: &str = r#" + WITH + seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + occupied AS ( + SELECT + po.office_type_id, + po.region_id, + COUNT(*) AS occupied_count + FROM falukant_data.political_office AS po + GROUP BY po.office_type_id, po.region_id + ) + SELECT + s.office_type_id, + s.region_id, + (s.seats_total - COALESCE(o.occupied_count, 0)) AS gaps + FROM seats AS s + LEFT JOIN occupied AS o + ON s.office_type_id = o.office_type_id + AND s.region_id = o.region_id + WHERE (s.seats_total - COALESCE(o.occupied_count, 0)) > 0; +"#; + +pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" + WITH + target_date AS ( + SELECT NOW()::date AS election_date + ), + expired_today AS ( + DELETE FROM falukant_data.political_office AS po + USING falukant_type.political_office_type AS pot + WHERE po.office_type_id = pot.id + AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date + = (SELECT election_date FROM target_date) + RETURNING + pot.id AS office_type_id, + po.region_id AS region_id + ), + gaps_per_region AS ( + SELECT + office_type_id, + region_id, + COUNT(*) AS gaps + FROM expired_today + GROUP BY office_type_id, region_id + ), + to_schedule AS ( + SELECT + g.office_type_id, + g.region_id, + g.gaps, + td.election_date + FROM gaps_per_region AS g + CROSS JOIN target_date AS td + WHERE NOT EXISTS ( + SELECT 1 + FROM falukant_data.election AS e + WHERE e.office_type_id = g.office_type_id + AND e.region_id = g.region_id + AND e.date::date = td.election_date + ) + ), + new_elections AS ( + INSERT INTO falukant_data.election + (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) + SELECT + ts.office_type_id, + ts.election_date, + ts.gaps, + NOW(), + NOW(), + ts.region_id + FROM to_schedule AS ts + RETURNING + id AS election_id, + region_id, + posts_to_fill + ) + SELECT + ne.election_id, + ne.region_id, + ne.posts_to_fill + FROM new_elections AS ne + ORDER BY ne.region_id, ne.election_id; +"#; + +pub const QUERY_INSERT_CANDIDATES: &str = r#" + INSERT INTO falukant_data.candidate + (election_id, character_id, created_at, updated_at) + SELECT + $1 AS election_id, + sub.id AS character_id, + NOW() AS created_at, + NOW() AS updated_at + FROM ( + WITH RECURSIVE region_tree AS ( + SELECT r.id + FROM falukant_data.region AS r + WHERE r.id = $2 + UNION ALL + SELECT r2.id + FROM falukant_data.region AS r2 + JOIN region_tree AS rt + ON r2.parent_id = rt.id + ) + SELECT ch.id + FROM falukant_data.character AS ch + JOIN region_tree AS rt2 + ON ch.region_id = rt2.id + WHERE ch.user_id IS NULL + AND ch.birthdate <= NOW() - INTERVAL '21 days' + AND ch.title_of_nobility IN ( + SELECT id + FROM falukant_type.title + WHERE label_tr != 'noncivil' + ) + ORDER BY RANDOM() + LIMIT ($3 * 2) + ) AS sub(id); +"#; + +pub const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" + SELECT + e.id AS election_id, + e.region_id AS region_id, + e.posts_to_fill + FROM falukant_data.election AS e + WHERE e.region_id IS NOT NULL + AND e.posts_to_fill > 0 + AND e.date::date >= CURRENT_DATE + AND NOT EXISTS ( + SELECT 1 + FROM falukant_data.candidate AS c + WHERE c.election_id = e.id + ); +"#; + +pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" + WITH + expired_offices AS ( + DELETE FROM falukant_data.political_office AS po + USING falukant_type.political_office_type AS pot + WHERE po.office_type_id = pot.id + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() + RETURNING + pot.id AS office_type_id, + po.region_id AS region_id + ), + distinct_types AS ( + SELECT DISTINCT office_type_id, region_id FROM expired_offices + ), + votes_per_candidate AS ( + SELECT + dt.office_type_id, + dt.region_id, + c.character_id, + COUNT(v.id) AS vote_count + FROM distinct_types AS dt + JOIN falukant_data.election AS e + ON e.office_type_id = dt.office_type_id + JOIN falukant_data.vote AS v + ON v.election_id = e.id + JOIN falukant_data.candidate AS c + ON c.election_id = e.id + AND c.id = v.candidate_id + WHERE e.date >= (NOW() - INTERVAL '30 days') + GROUP BY dt.office_type_id, dt.region_id, c.character_id + ), + ranked_winners AS ( + SELECT + vpc.office_type_id, + vpc.region_id, + vpc.character_id, + ROW_NUMBER() OVER ( + PARTITION BY vpc.office_type_id, vpc.region_id + ORDER BY vpc.vote_count DESC + ) AS rn + FROM votes_per_candidate AS vpc + ), + selected_winners AS ( + SELECT + rw.office_type_id, + rw.region_id, + rw.character_id + FROM ranked_winners AS rw + JOIN falukant_type.political_office_type AS pot + ON pot.id = rw.office_type_id + WHERE rw.rn <= pot.seats_per_region + ), + insert_winners AS ( + INSERT INTO falukant_data.political_office + (office_type_id, character_id, created_at, updated_at, region_id) + SELECT + sw.office_type_id, + sw.character_id, + NOW(), + NOW(), + sw.region_id + FROM selected_winners AS sw + WHERE NOT EXISTS ( + SELECT 1 + FROM falukant_data.political_office po2 + JOIN falukant_type.political_office_type pot2 ON po2.office_type_id = pot2.id + WHERE po2.character_id = sw.character_id + AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) > NOW() + INTERVAL '2 days' + ) + RETURNING id AS new_office_id, office_type_id, character_id, region_id + ), + count_inserted AS ( + SELECT + office_type_id, + region_id, + COUNT(*) AS inserted_count + FROM insert_winners + GROUP BY office_type_id, region_id + ), + needed_to_fill AS ( + SELECT + dt.office_type_id, + dt.region_id, + (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps + FROM distinct_types AS dt + JOIN falukant_type.political_office_type AS pot + ON pot.id = dt.office_type_id + LEFT JOIN count_inserted AS ci + ON ci.office_type_id = dt.office_type_id + AND ci.region_id = dt.region_id + WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0 + ), + random_candidates AS ( + SELECT + rtf.office_type_id, + rtf.region_id, + ch.id AS character_id, + ROW_NUMBER() OVER ( + PARTITION BY rtf.office_type_id, rtf.region_id + ORDER BY RANDOM() + ) AS rn + FROM needed_to_fill AS rtf + JOIN falukant_data.character AS ch + ON ch.region_id = rtf.region_id + AND ch.user_id IS NULL + AND ch.birthdate <= NOW() - INTERVAL '21 days' + AND ch.title_of_nobility IN ( + SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil' + ) + AND NOT EXISTS ( + SELECT 1 + FROM falukant_data.political_office AS po2 + JOIN falukant_type.political_office_type AS pot2 + ON pot2.id = po2.office_type_id + WHERE po2.character_id = ch.id + AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) > + NOW() + INTERVAL '2 days' + ) + ), + insert_random AS ( + INSERT INTO falukant_data.political_office + (office_type_id, character_id, created_at, updated_at, region_id) + SELECT + rc.office_type_id, + rc.character_id, + NOW(), + NOW(), + rc.region_id + FROM random_candidates AS rc + JOIN needed_to_fill AS rtf + ON rtf.office_type_id = rc.office_type_id + AND rtf.region_id = rc.region_id + WHERE rc.rn <= rtf.gaps + RETURNING id AS new_office_id, office_type_id, character_id, region_id + ) + SELECT + new_office_id AS office_id, + office_type_id, + character_id, + region_id + FROM insert_winners + UNION ALL + SELECT + new_office_id AS office_id, + office_type_id, + character_id, + region_id + FROM insert_random; +"#; + +pub const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#" + WITH RECURSIVE region_tree AS ( + SELECT id + FROM falukant_data.region + WHERE id = $1 + UNION ALL + SELECT r2.id + FROM falukant_data.region AS r2 + JOIN region_tree AS rt + ON r2.parent_id = rt.id + ) + SELECT DISTINCT ch.user_id + FROM falukant_data.character AS ch + JOIN region_tree AS rt2 + ON ch.region_id = rt2.id + WHERE ch.user_id IS NOT NULL; +"#; + +pub const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + SELECT + ch.user_id, + 'notify_office_expiring', + NOW(), + NOW() + FROM falukant_data.political_office AS po + JOIN falukant_type.political_office_type AS pot + ON po.office_type_id = pot.id + JOIN falukant_data.character AS ch + ON ch.id = po.character_id + WHERE ch.user_id IS NOT NULL + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) + BETWEEN (NOW() + INTERVAL '2 days') + AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); +"#; + +pub const QUERY_NOTIFY_ELECTION_CREATED: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + VALUES + ($1, 'notify_election_created', NOW(), NOW()); +"#; + +pub const QUERY_NOTIFY_OFFICE_FILLED: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + VALUES + ((SELECT user_id FROM falukant_data.character WHERE id = $1), 'notify_office_filled', NOW(), NOW()); +"#; + +pub const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.political_office AS po + JOIN falukant_type.political_office_type AS pot + ON po.office_type_id = pot.id + JOIN falukant_data.character AS ch + ON po.character_id = ch.id + WHERE ch.user_id IS NOT NULL + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) + BETWEEN (NOW() + INTERVAL '2 days') + AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); +"#; + +pub const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.election AS e + JOIN falukant_data.character AS ch + ON ch.region_id = e.region_id + WHERE ch.user_id IS NOT NULL + AND e.date >= NOW() - INTERVAL '1 day'; +"#; + +pub const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.political_office AS po + JOIN falukant_data.character AS ch + ON po.character_id = ch.id + WHERE ch.user_id IS NOT NULL + AND po.created_at >= NOW() - INTERVAL '1 minute'; +"#; + +pub const QUERY_PROCESS_ELECTIONS: &str = r#" + SELECT office_id, office_type_id, character_id, region_id + FROM falukant_data.process_elections(); +"#; + +pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" + WITH seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + ranked AS ( + SELECT + po.id, + po.office_type_id, + po.region_id, + s.seats_total, + ROW_NUMBER() OVER ( + PARTITION BY po.office_type_id, po.region_id + ORDER BY po.created_at DESC + ) AS rn + FROM falukant_data.political_office AS po + JOIN seats AS s + ON s.office_type_id = po.office_type_id + AND s.region_id = po.region_id + ), + to_delete AS ( + SELECT id + FROM ranked + WHERE rn > seats_total + ) + DELETE FROM falukant_data.political_office + WHERE id IN (SELECT id FROM to_delete); +"#; + +pub const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#" + UPDATE falukant_data."character" + SET health = $1 + WHERE id = $2; +"#; + +pub const QUERY_UPDATE_MOOD: &str = r#" + UPDATE falukant_data."character" AS c + SET mood_id = falukant_data.get_random_mood_id() + WHERE c.health > 0 + AND random() < (1.0 / 50.0); +"#; + +pub const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#" + SELECT id, product_id, producer_id, quantity + FROM falukant_log.production p + WHERE p.production_timestamp::date < current_date; +"#; + +pub const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#" + SELECT fu.id AS user_id, + c.id AS character_id, + c2.id AS director_id + FROM falukant_data.falukant_user fu + JOIN falukant_data.character c + ON c.user_id = fu.id + LEFT JOIN falukant_data.director d + ON d.employer_user_id = fu.id + LEFT JOIN falukant_data.character c2 + ON c2.id = d.director_character_id + WHERE fu.id = $1; +"#; + +pub const QUERY_UPDATE_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge + SET knowledge = LEAST(knowledge + $3, 100) + WHERE character_id = $1 + AND product_id = $2; +"#; + +pub const QUERY_DELETE_LOG_ENTRY: &str = r#" + DELETE FROM falukant_log.production + WHERE id = $1; +"#; + +pub const QUERY_GET_OPEN_CREDITS: &str = r#" + SELECT + c.id AS credit_id, + c.amount, + c.remaining_amount, + c.interest_rate, + fu.id AS user_id, + fu.money, + c2.id AS character_id, + dp.created_at AS debitor_prism_start, + dp.created_at::date < current_date AS prism_started_previously + FROM falukant_data.credit c + JOIN falukant_data.falukant_user fu + ON fu.id = c.falukant_user_id + JOIN falukant_data.character c2 + ON c2.user_id = c.falukant_user_id + LEFT JOIN falukant_data.debtors_prism dp + ON dp.character_id = c2.id + WHERE c.remaining_amount > 0 + AND c.updated_at::date < current_date; +"#; + +pub const QUERY_UPDATE_CREDIT: &str = r#" + UPDATE falukant_data.credit c + SET remaining_amount = $1 + WHERE falukant_user_id = $2; +"#; + +pub const QUERY_CLEANUP_CREDITS: &str = r#" + DELETE FROM falukant_data.credit + WHERE remaining_amount <= 0.01; +"#; + +pub const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#" + INSERT INTO falukant_data.debtors_prism (character_id) + VALUES ($1); +"#; + +pub const QUERY_RANDOM_HEIR: &str = r#" + WITH chosen AS ( + SELECT + cr.id AS relation_id, + cr.child_character_id + FROM + falukant_data.child_relation AS cr + JOIN + falukant_data.character AS ch + ON ch.id = cr.child_character_id + WHERE + (cr.father_character_id = $1 OR cr.mother_character_id = $1) + AND ch.region_id = ( + SELECT region_id + FROM falukant_data.character + WHERE id = $1 + ) + AND ch.birthdate >= NOW() - INTERVAL '10 days' + AND ch.title_of_nobility = ( + SELECT id + FROM falukant_type.title + WHERE label_tr = 'noncivil' + ) + ORDER BY RANDOM() + LIMIT 1 + ) + UPDATE + falukant_data.child_relation AS cr2 + SET + is_heir = TRUE, + updated_at = NOW() + FROM + chosen + WHERE + cr2.id = chosen.relation_id + RETURNING + chosen.child_character_id; +"#; + +pub const QUERY_UPDATE_USER_MONEY: &str = r#" + UPDATE falukant_data.falukant_user + SET money = $1, + updated_at = NOW() + WHERE id = $2; +"#; + +pub const QUERY_GET_FALUKANT_USER_ID: &str = r#" + SELECT user_id + FROM falukant_data.character + WHERE id = $1 + LIMIT 1; +"#; + +pub const QUERY_AUTOBATISM: &str = r#" + UPDATE falukant_data.child_relation + SET name_set = TRUE + WHERE id IN ( + SELECT cr.id + FROM falukant_data.child_relation cr + JOIN falukant_data.character c + ON c.id = cr.child_character_id + WHERE cr.name_set = FALSE + AND c.birthdate < current_date - INTERVAL '5 days' + ); +"#; + +pub const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" + SELECT + r.character1_id AS father_cid, + r.character2_id AS mother_cid, + c1.title_of_nobility, + c1.last_name, + c1.region_id, + fu1.id AS father_uid, + fu2.id AS mother_uid, + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days, + 100.0 / + (1 + EXP( + 0.0647 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 0.0591 + )) AS prob_pct + FROM falukant_data.relationship r + JOIN falukant_type.relationship r2 + ON r2.id = r.relationship_type_id + AND r2.tr = 'married' + JOIN falukant_data.character c1 + ON c1.id = r.character1_id + JOIN falukant_data.character c2 + ON c2.id = r.character2_id + LEFT JOIN falukant_data.falukant_user fu1 + ON fu1.id = c1.user_id + LEFT JOIN falukant_data.falukant_user fu2 + ON fu2.id = c2.user_id + WHERE random() * 100 < ( + 100.0 / + (1 + EXP( + 0.11166347 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 2.638267 + )) + ) / 2; +"#; + +pub const QUERY_INSERT_CHILD: &str = r#" + INSERT INTO falukant_data.character ( + user_id, + region_id, + first_name, + last_name, + birthdate, + gender, + title_of_nobility, + mood_id, + created_at, + updated_at + ) VALUES ( + NULL, + $1::int, + ( + SELECT id + FROM falukant_predefine.firstname + WHERE gender = $2 + ORDER BY RANDOM() + LIMIT 1 + ), + $3::int, + NOW(), + $2::varchar, + $4::int, + ( + SELECT id + FROM falukant_type.mood + ORDER BY RANDOM() + LIMIT 1 + ), + NOW(), + NOW() + ) + RETURNING id AS child_cid; +"#; + +pub const QUERY_INSERT_CHILD_RELATION: &str = r#" + INSERT INTO falukant_data.child_relation ( + father_character_id, + mother_character_id, + child_character_id, + name_set, + created_at, + updated_at + ) + VALUES ( + $1::int, + $2::int, + $3::int, + FALSE, + NOW(), NOW() + ); +"#; + +pub const QUERY_DELETE_KNOWLEDGE: &str = r#" + DELETE FROM falukant_data.knowledge + WHERE character_id = $1; +"#; + +pub const QUERY_DELETE_DEBTORS_PRISM: &str = r#" + DELETE FROM falukant_data.debtors_prism + WHERE character_id = $1; +"#; + +pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" + WITH removed AS ( + DELETE FROM falukant_data.political_office + WHERE character_id = $1 + RETURNING office_type_id, region_id + ), + affected AS ( + SELECT DISTINCT office_type_id, region_id + FROM removed + ), + seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + JOIN affected AS a + ON a.office_type_id = pot.id + AND a.region_id = rt.id + ), + ranked AS ( + SELECT + po.id, + po.office_type_id, + po.region_id, + s.seats_total, + ROW_NUMBER() OVER ( + PARTITION BY po.office_type_id, po.region_id + ORDER BY po.created_at DESC + ) AS rn + FROM falukant_data.political_office AS po + JOIN seats AS s + ON s.office_type_id = po.office_type_id + AND s.region_id = po.region_id + ), + to_delete AS ( + SELECT id + FROM ranked + WHERE rn > seats_total + ) + DELETE FROM falukant_data.political_office + WHERE id IN (SELECT id FROM to_delete); +"#; + +pub const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#" + DELETE FROM falukant_data.election_candidate + WHERE character_id = $1; +"#; + +pub const QUERY_GET_STOCK_TYPE_ID: &str = r#" +SELECT id FROM falukant_type.stock WHERE label_tr = $1 LIMIT 1; +"#; + +pub const QUERY_GET_INVENTORY_ITEMS: &str = r#" +SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id FROM falukant_data.inventory i JOIN falukant_data.stock s ON i.stock_id = s.id JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.region_id = $1 AND s.stock_type_id = $2; +"#; + +pub const QUERY_REDUCE_INVENTORY: &str = r#" +UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; +"#; + +pub const QUERY_DELETE_INVENTORY: &str = r#" +DELETE FROM falukant_data.inventory WHERE stock_id = $1; +"#; + +pub const QUERY_DELETE_STOCK: &str = r#" +DELETE FROM falukant_data.stock WHERE id = $1; +"#; + +pub const QUERY_GET_STOCK_INVENTORY: &str = r#" +SELECT id, quantity FROM falukant_data.inventory WHERE stock_id = $1; +"#; + +pub const QUERY_CAP_INVENTORY: &str = r#" +UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; +"#; + +pub const QUERY_GET_USER_INVENTORY_ITEMS: &str = r#" +SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id +FROM falukant_data.inventory i +JOIN falukant_data.stock s ON i.stock_id = s.id +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.falukant_user_id = $1 AND s.stock_type_id = $2; +"#; +// Produce worker queries +pub const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#" + SELECT + p.id AS production_id, + p.branch_id, + p.product_id, + p.quantity, + p.start_timestamp, + pr.production_time, + br.region_id, + br.falukant_user_id AS user_id, + ROUND( + GREATEST( + 0, + LEAST( + 100, + ( + (COALESCE(k.knowledge, 0) * 0.75 + + COALESCE(k2.knowledge, 0) * 0.25) + * COALESCE(pwe.quality_effect, 100) / 100.0 + ) + ) + ) + )::int AS quality + FROM falukant_data.production p + JOIN falukant_type.product pr + ON p.product_id = pr.id + JOIN falukant_data.branch br + ON p.branch_id = br.id + JOIN falukant_data.character c + ON c.user_id = br.falukant_user_id + LEFT JOIN falukant_data.knowledge k + ON p.product_id = k.product_id + AND k.character_id = c.id + LEFT JOIN falukant_data.director d + ON d.employer_user_id = c.user_id + LEFT JOIN falukant_data.knowledge k2 + ON k2.character_id = d.director_character_id + AND k2.product_id = p.product_id + LEFT JOIN falukant_data.weather w + ON w.region_id = br.region_id + LEFT JOIN falukant_type.product_weather_effect pwe + ON pwe.product_id = p.product_id + AND pwe.weather_type_id = w.weather_type_id + -- Wetter-Effekte derzeit aus der Qualitätsberechnung entfernt + WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW() + ORDER BY p.start_timestamp; +"#; + +pub const QUERY_DELETE_PRODUCTION: &str = r#" + DELETE FROM falukant_data.production + WHERE id = $1; +"#; + +pub const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#" + INSERT INTO falukant_log.production ( + region_id, + product_id, + quantity, + producer_id, + production_date + ) VALUES ($1, $2, $3, $4, CURRENT_DATE) + ON CONFLICT (producer_id, product_id, region_id, production_date) + DO UPDATE + SET quantity = falukant_log.production.quantity + EXCLUDED.quantity; +"#; + +pub const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#" + INSERT INTO falukant_log.notification ( + user_id, + tr, + shown, + created_at, + updated_at + ) VALUES ($1, $2, FALSE, NOW(), NOW()); +"#; + +// Aliases for personal variants (keeps original prepared statement names used in events.worker) +pub const QUERY_REDUCE_INVENTORY_PERSONAL: &str = QUERY_REDUCE_INVENTORY; +pub const QUERY_DELETE_INVENTORY_PERSONAL: &str = QUERY_DELETE_INVENTORY; +pub const QUERY_DELETE_STOCK_PERSONAL: &str = QUERY_DELETE_STOCK; +pub const QUERY_GET_STOCK_INVENTORY_PERSONAL: &str = QUERY_GET_STOCK_INVENTORY; +pub const QUERY_CAP_INVENTORY_PERSONAL: &str = QUERY_CAP_INVENTORY; + +// value_recalculation worker queries +pub const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + 1) + FROM falukant_data.character c + JOIN falukant_log.production p + ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' + WHERE c.id = k.character_id + AND c.user_id = 18 + AND k.product_id = 10; +"#; + +pub const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#" + DELETE FROM falukant_log.production flp + WHERE DATE(flp.production_timestamp) < CURRENT_DATE; +"#; + +pub const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#" + SELECT p.producer_id + FROM falukant_log.production p + WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' + GROUP BY producer_id; +"#; + +pub const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#" + UPDATE falukant_data.town_product_worth tpw + SET worth_percent = + GREATEST( + 0, + LEAST( + CASE + WHEN s.quantity > COALESCE(s.avg_sells, 0) * 1.05 THEN tpw.worth_percent + 1 + WHEN s.quantity < COALESCE(s.avg_sells, 0) * 0.95 THEN tpw.worth_percent - 1 + ELSE tpw.worth_percent + END, + 100 + ) + ) + FROM ( + SELECT region_id, + product_id, + quantity, + (SELECT AVG(quantity) + FROM falukant_log.sell avs + WHERE avs.product_id = s.product_id) AS avg_sells + FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' + ) s + WHERE tpw.region_id = s.region_id + AND tpw.product_id = s.product_id; +"#; + +pub const QUERY_DELETE_REGION_SELL_PRICE: &str = r#" + DELETE FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) < CURRENT_DATE; +"#; + +pub const QUERY_GET_SELL_REGIONS: &str = r#" + SELECT s.region_id + FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' + GROUP BY region_id; +"#; + +pub const QUERY_HOURLY_PRICE_RECALCULATION: &str = r#" + WITH city_sales AS ( + SELECT s.region_id, s.product_id, SUM(s.quantity) AS total_sold + FROM falukant_log.sell s + WHERE s.sell_timestamp >= NOW() - INTERVAL '1 hour' + GROUP BY s.region_id, s.product_id + ), + world_avg AS ( + SELECT product_id, AVG(total_sold) AS avg_sold + FROM city_sales + GROUP BY product_id + ), + adjustments AS ( + SELECT + cs.region_id, + cs.product_id, + CASE + WHEN cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 THEN 1.10 + WHEN cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 THEN 0.90 + ELSE 1.00 + END AS factor + FROM city_sales cs + JOIN world_avg wa ON wa.product_id = cs.product_id + ) + UPDATE falukant_data.town_product_worth tpw + SET worth_percent = GREATEST(0, LEAST(100, tpw.worth_percent * adj.factor)) + FROM adjustments adj + WHERE tpw.region_id = adj.region_id + AND tpw.product_id = adj.product_id; +"#; + +pub const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" + WITH updated_relations AS ( + UPDATE falukant_data.relationship AS rel + SET relationship_type_id = ( + SELECT id + FROM falukant_type.relationship AS rt + WHERE rt.tr = 'married' + ) + WHERE rel.id IN ( + SELECT rel2.id + FROM falukant_data.party AS p + JOIN falukant_type.party AS pt + ON pt.id = p.party_type_id + AND pt.tr = 'wedding' + JOIN falukant_data.falukant_user AS fu + ON fu.id = p.falukant_user_id + JOIN falukant_data.character AS c + ON c.user_id = fu.id + JOIN falukant_data.relationship AS rel2 + ON rel2.character1_id = c.id + OR rel2.character2_id = c.id + JOIN falukant_type.relationship AS rt2 + ON rt2.id = rel2.relationship_type_id + AND rt2.tr = 'engaged' + WHERE p.created_at <= NOW() - INTERVAL '1 day' + ) + RETURNING character1_id, character2_id + ) + SELECT + c1.user_id AS character1_user, + c2.user_id AS character2_user + FROM updated_relations AS ur + JOIN falukant_data.character AS c1 + ON c1.id = ur.character1_id + JOIN falukant_data.character AS c2 + ON c2.id = ur.character2_id; +"#; + +pub const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#" + SELECT + l.id, + l.associated_falukant_user_id, + l.associated_learning_character_id, + l.learn_all_products, + l.learning_recipient_id, + l.product_id, + lr.tr + FROM falukant_data.learning l + JOIN falukant_type.learn_recipient lr + ON lr.id = l.learning_recipient_id + WHERE l.learning_is_executed = FALSE + AND l.created_at + INTERVAL '1 day' < NOW(); +"#; + +pub const QUERY_GET_OWN_CHARACTER_ID: &str = r#" + SELECT id + FROM falukant_data.character c + WHERE c.user_id = $1; +"#; + +pub const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + $1) + WHERE k.character_id = $2 + AND k.product_id = $3; +"#; + +pub const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + $1) + WHERE k.character_id = $2; +"#; + +pub const QUERY_SET_LEARNING_DONE: &str = r#" + UPDATE falukant_data.learning + SET learning_is_executed = TRUE, + updated_at = NOW() + WHERE id = $1; +"#; + +// Church Office Queries +pub const QUERY_FIND_AVAILABLE_CHURCH_OFFICES: &str = r#" + SELECT + cot.id AS office_type_id, + cot.name AS office_type_name, + cot.seats_per_region, + cot.region_type, + r.id AS region_id, + COUNT(co.id) AS occupied_seats + FROM falukant_type.church_office_type cot + CROSS JOIN falukant_data.region r + JOIN falukant_type.region tr ON r.region_type_id = tr.id + LEFT JOIN falukant_data.church_office co + ON cot.id = co.office_type_id + AND co.region_id = r.id + WHERE tr.label_tr = cot.region_type + GROUP BY cot.id, cot.name, cot.seats_per_region, cot.region_type, r.id + HAVING COUNT(co.id) < cot.seats_per_region + ORDER BY cot.hierarchy_level ASC, r.id; +"#; + +pub const QUERY_FIND_CHURCH_SUPERVISOR: &str = r#" + SELECT + co.id AS office_id, + co.character_id AS supervisor_character_id, + co.region_id, + cot.hierarchy_level + FROM falukant_data.church_office co + JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id + WHERE co.region_id = $1 + AND cot.hierarchy_level > ( + SELECT hierarchy_level + FROM falukant_type.church_office_type + WHERE id = $2 + ) + ORDER BY cot.hierarchy_level ASC + LIMIT 1; +"#; + +pub const QUERY_GET_CHURCH_OFFICE_REQUIREMENTS: &str = r#" + SELECT + id, + office_type_id, + prerequisite_office_type_id, + min_title_level + FROM falukant_predefine.church_office_requirement + WHERE office_type_id = $1; +"#; + +pub const QUERY_GET_PENDING_CHURCH_APPLICATIONS: &str = r#" + SELECT + ca.id AS application_id, + ca.office_type_id, + ca.character_id AS applicant_character_id, + ca.region_id, + ca.supervisor_id, + cot.name AS office_type_name, + cot.hierarchy_level + FROM falukant_data.church_application ca + JOIN falukant_type.church_office_type cot ON ca.office_type_id = cot.id + WHERE ca.status = 'pending' + AND ca.supervisor_id = $1 + ORDER BY cot.hierarchy_level ASC, ca.created_at ASC; +"#; + +pub const QUERY_CHECK_CHARACTER_ELIGIBILITY: &str = r#" + WITH character_info AS ( + SELECT + c.id AS character_id, + c.title_of_nobility, + t.level AS title_level, + EXISTS( + SELECT 1 + FROM falukant_data.church_office co2 + WHERE co2.character_id = c.id + ) AS has_office + FROM falukant_data.character c + LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id + WHERE c.id = $1 + ), + prerequisite_check AS ( + SELECT + CASE + WHEN $2::int IS NULL THEN TRUE + ELSE EXISTS( + SELECT 1 + FROM falukant_data.church_office co + WHERE co.character_id = $1 + AND co.office_type_id = $2::int + ) + END AS has_prerequisite + ) + SELECT + ci.character_id, + ci.title_level, + ci.has_office, + pc.has_prerequisite, + CASE + WHEN $3::int IS NULL THEN TRUE + ELSE COALESCE(ci.title_level, 0) >= $3::int + END AS meets_title_requirement + FROM character_info ci + CROSS JOIN prerequisite_check pc; +"#; + +pub const QUERY_APPROVE_CHURCH_APPLICATION: &str = r#" + WITH updated_application AS ( + UPDATE falukant_data.church_application + SET status = 'approved', + decision_date = NOW(), + updated_at = NOW() + WHERE id = $1 + AND status = 'pending' + RETURNING + office_type_id, + character_id, + region_id, + supervisor_id + ), + inserted_office AS ( + INSERT INTO falukant_data.church_office + (office_type_id, character_id, region_id, supervisor_id, created_at, updated_at) + SELECT + office_type_id, + character_id, + region_id, + supervisor_id, + NOW(), + NOW() + FROM updated_application + WHERE NOT EXISTS( + SELECT 1 + FROM falukant_data.church_office co + WHERE co.office_type_id = updated_application.office_type_id + AND co.region_id = updated_application.region_id + AND co.character_id = updated_application.character_id + ) + RETURNING id, office_type_id, character_id, region_id + ), + remove_lower_ranked AS ( + DELETE FROM falukant_data.church_office + WHERE id IN ( + SELECT co.id + FROM falukant_data.church_office co + JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id + WHERE co.character_id IN (SELECT character_id FROM inserted_office) + AND EXISTS ( + SELECT 1 + FROM falukant_data.church_office co2 + JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id + WHERE co2.character_id = co.character_id + AND cot2.hierarchy_level > cot.hierarchy_level + ) + ) + ) + SELECT + id AS office_id, + office_type_id, + character_id, + region_id + FROM inserted_office; +"#; + +pub const QUERY_REJECT_CHURCH_APPLICATION: &str = r#" + UPDATE falukant_data.church_application + SET status = 'rejected', + decision_date = NOW(), + updated_at = NOW() + WHERE id = $1 + AND status = 'pending' + RETURNING id; +"#; + +pub const QUERY_CREATE_CHURCH_APPLICATION_JOB: &str = r#" + INSERT INTO falukant_data.church_application + (office_type_id, character_id, region_id, supervisor_id, status, created_at, updated_at) + SELECT + $1::int AS office_type_id, + $2::int AS character_id, + $3::int AS region_id, + $4::int AS supervisor_id, + 'pending' AS status, + NOW() AS created_at, + NOW() AS updated_at + WHERE NOT EXISTS( + SELECT 1 + FROM falukant_data.church_application ca + WHERE ca.office_type_id = $1::int + AND ca.character_id = $2::int + AND ca.region_id = $3::int + AND ca.status = 'pending' + ) + RETURNING id; +"#; + +pub const QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE: &str = r#" + SELECT DISTINCT + c.id AS character_id, + c.user_id, + c.region_id, + c.title_of_nobility, + t.level AS title_level + FROM falukant_data.character c + LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id + WHERE c.region_id = $1 + AND c.health > 0 + AND NOT EXISTS( + SELECT 1 + FROM falukant_data.church_office co + WHERE co.character_id = c.id + ) + ORDER BY RANDOM() + LIMIT $2; +"#; + +// Prüft und entfernt niedrigerrangige politische Ämter wenn Character mehrere hat +pub const QUERY_REMOVE_LOWER_RANKED_POLITICAL_OFFICES: &str = r#" + WITH character_offices AS ( + SELECT + po.id AS office_id, + po.character_id, + po.office_type_id, + pot.hierarchy_level, + ROW_NUMBER() OVER ( + PARTITION BY po.character_id + ORDER BY pot.hierarchy_level DESC, po.created_at DESC + ) AS rn + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON po.office_type_id = pot.id + WHERE (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW() + INTERVAL '2 days' + ), + to_delete AS ( + SELECT office_id + FROM character_offices + WHERE rn > 1 + ) + DELETE FROM falukant_data.political_office + WHERE id IN (SELECT office_id FROM to_delete); +"#; + +// Prüft und entfernt niedrigerrangige Church Offices wenn Character mehrere hat +pub const QUERY_REMOVE_LOWER_RANKED_CHURCH_OFFICES: &str = r#" + WITH character_offices AS ( + SELECT + co.id AS office_id, + co.character_id, + co.office_type_id, + cot.hierarchy_level, + ROW_NUMBER() OVER ( + PARTITION BY co.character_id + ORDER BY cot.hierarchy_level DESC, co.created_at DESC + ) AS rn + FROM falukant_data.church_office co + JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id + ), + to_delete AS ( + SELECT office_id + FROM character_offices + WHERE rn > 1 + ) + DELETE FROM falukant_data.church_office + WHERE id IN (SELECT office_id FROM to_delete); +"#; + +// Prüft ob Character bereits ein politisches Amt hat (für insert_winners) +pub const QUERY_CHECK_EXISTING_POLITICAL_OFFICE: &str = r#" + SELECT EXISTS( + SELECT 1 + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON po.office_type_id = pot.id + WHERE po.character_id = $1 + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW() + INTERVAL '2 days' + ) AS has_office; +"#; + +// Prüft ob Character bereits ein Church Office hat +pub const QUERY_CHECK_EXISTING_CHURCH_OFFICE: &str = r#" + SELECT EXISTS( + SELECT 1 + FROM falukant_data.church_office co + WHERE co.character_id = $1 + ) AS has_office; +"#; +