diff --git a/src/worker/director.rs b/src/worker/director.rs index d3ba11e..bf7a7dc 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -1,4 +1,5 @@ use crate::db::{DbConnection, DbError, Row}; +use std::collections::HashMap; use crate::message_broker::MessageBroker; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -46,6 +47,12 @@ struct SalaryItem { income: i32, } +#[derive(Debug, Clone)] +struct TransportVehicle { + id: i32, + capacity: i32, +} + pub struct DirectorWorker { base: BaseWorker, last_run: Option, @@ -171,6 +178,46 @@ const QUERY_ADD_SELL_LOG: &str = r#" SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity; "#; +// Regionale Verkaufswürdigkeit pro Produkt/Region für alle Branches eines Users +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; +"#; + +// Verfügbare Transportmittel für eine Route (source_region -> target_region) +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 + AND v.condition > 0 + AND v.available_from <= NOW(); +"#; + +// Transport-Eintrag anlegen +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()); +"#; + const QUERY_GET_SALARY_TO_PAY: &str = r#" SELECT d.id, d.employer_user_id, d.income FROM falukant_data.director d @@ -417,8 +464,48 @@ impl DirectorWorker { conn.prepare("remove_inventory", QUERY_REMOVE_INVENTORY)?; conn.prepare("add_sell_log", QUERY_ADD_SELL_LOG)?; + // Falls es nichts zu verkaufen gibt, können wir sofort zurückkehren. + if items.is_empty() { + return Ok(()); + } + + // Für alle Items dieses Directors sollten die user_id-Felder identisch + // sein (Arbeitgeber des Directors). + let falukant_user_id = items[0].user_id; + + // Vor dem eigentlichen Verkauf versucht der Director, lohnende + // Transporte zu planen. Dabei werden: + // - ggf. Transport-Einträge erzeugt + // - Inventar-Mengen reduziert + // Die zurückgegebenen Mengen werden dann lokal verkauft. + for item in items.iter_mut() { + let shipped = self.plan_transports_for_item( + &mut conn, + falukant_user_id, + item, + )?; + + if shipped > 0 { + if shipped >= item.quantity { + // Alles wurde in Transporte umgewandelt, lokal nichts mehr zu verkaufen. + item.quantity = 0; + } else { + // Inventar-Menge in der DB reduzieren und im Item anpassen. + let remaining = item.quantity - shipped; + Self::update_inventory_quantity(&mut conn, item.id, remaining)?; + item.quantity = remaining; + } + } + } + + // Anschließend lokale Verkäufe für die verbleibenden Mengen durchführen. for item in items.drain(..) { - self.sell_single_inventory_item(&mut conn, &item)?; + if item.quantity > 0 { + self.sell_single_inventory_item(&mut conn, &item)?; + } else { + // Falls die Menge auf 0 gesetzt wurde, das Inventar ggf. aufräumen. + conn.execute("remove_inventory", &[&item.id])?; + } } Ok(()) @@ -483,6 +570,262 @@ impl DirectorWorker { Ok(()) } + /// Plant ggf. Transporte für ein einzelnes Inventar-Item und gibt die + /// Menge zurück, die tatsächlich in Transporte umgewandelt wurde. + /// + /// Logik: + /// - Ermittle regionale "worth_percent"-Werte für das Produkt in allen + /// Branch-Regionen des Users. + /// - Berechne lokalen Stückpreis (inkl. Qualität) und für jede andere + /// Region einen potentiellen Stückpreis. + /// - Prüfe für jede Zielregion: + /// * Gibt es verfügbare Transportmittel für diese Route? + /// * Ist der Mehrerlös (deltaPrice * Menge) größer als die + /// Transportkosten (max(1, totalValue * 0.01))? + /// - Wähle die Zielregion mit dem größten positiven Nettogewinn und + /// erzeuge entsprechende Transporte (begrenzt durch Fahrzeugkapazität). + fn plan_transports_for_item( + &mut self, + conn: &mut DbConnection, + falukant_user_id: i32, + item: &mut InventoryItem, + ) -> Result { + // Sicherheitscheck + if item.quantity <= 0 { + return Ok(0); + } + + // Regionale worth_percent-Werte für dieses Produkt laden + conn.prepare( + "get_region_worth_for_product", + QUERY_GET_REGION_WORTH_FOR_PRODUCT, + )?; + let rows = conn.execute( + "get_region_worth_for_product", + &[&falukant_user_id, &item.product_id], + )?; + + if rows.is_empty() { + return Ok(0); + } + + let mut worth_by_region: HashMap = HashMap::new(); + for row in rows { + let region_id = row + .get("region_id") + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1); + let percent = row + .get("worth_percent") + .and_then(|v| v.parse::().ok()) + .unwrap_or(100.0); + + if region_id >= 0 { + worth_by_region.insert(region_id, percent); + } + } + + if worth_by_region.is_empty() { + return Ok(0); + } + + // Lokalen Stückpreis berechnen + let local_percent = worth_by_region + .get(&item.region_id) + .copied() + .unwrap_or(100.0); + + let local_sell_cost = item.sell_cost * local_percent / 100.0; + let local_min_price = local_sell_cost * 0.6; + let quality_factor = item.quality as f64 / 100.0; + let local_piece_price = + local_min_price + (local_sell_cost - local_min_price) * quality_factor; + + let mut best_target_region: Option = None; + let mut best_quantity: i32 = 0; + let mut best_remote_piece_price: f64 = 0.0; + let mut best_gain: f64 = 0.0; + + // Für jede andere Region prüfen, ob sich ein Transport lohnt. + for (®ion_id, &remote_percent) in &worth_by_region { + if region_id == item.region_id { + continue; + } + + // Remote-Stückpreis + let remote_sell_cost = item.sell_cost * remote_percent / 100.0; + let remote_min_price = remote_sell_cost * 0.6; + let remote_piece_price = + remote_min_price + (remote_sell_cost - remote_min_price) * quality_factor; + + let delta_per_unit = remote_piece_price - local_piece_price; + if delta_per_unit <= 0.0 { + continue; + } + + // Verfügbare Transportmittel für diese Route abfragen + let vehicles = Self::get_transport_vehicles_for_route( + conn, + falukant_user_id, + item.region_id, + region_id, + )?; + + if vehicles.is_empty() { + continue; + } + + // Maximale transportierbare Menge anhand der Kapazität ermitteln + let mut max_capacity: i32 = 0; + for v in &vehicles { + max_capacity = max_capacity.saturating_add(v.capacity); + } + + if max_capacity <= 0 { + continue; + } + + let qty = std::cmp::min(item.quantity, max_capacity); + if qty <= 0 { + continue; + } + + let extra_revenue = delta_per_unit * qty as f64; + let total_value = remote_piece_price * qty as f64; + let transport_cost = total_value * 0.01_f64; + // Kostenformel: max(0.01, totalValue * 0.01) + let transport_cost = if transport_cost < 0.01 { + 0.01 + } else { + transport_cost + }; + + let net_gain = extra_revenue - transport_cost; + if net_gain <= 0.0 { + continue; + } + + if net_gain > best_gain { + best_gain = net_gain; + best_target_region = Some(region_id); + best_quantity = qty; + best_remote_piece_price = remote_piece_price; + } + } + + // Kein lohnender Transport gefunden + let target_region = match best_target_region { + Some(r) => r, + None => return Ok(0), + }; + + if best_quantity <= 0 { + return Ok(0); + } + + // Nochmals verfügbare Transportmittel für die gewählte Route laden + let vehicles = Self::get_transport_vehicles_for_route( + conn, + falukant_user_id, + item.region_id, + target_region, + )?; + + if vehicles.is_empty() { + return Ok(0); + } + + // Transporte anlegen, begrenzt durch best_quantity und Kapazitäten + conn.prepare("insert_transport", QUERY_INSERT_TRANSPORT)?; + + let mut remaining = best_quantity; + for v in &vehicles { + if remaining <= 0 { + break; + } + let size = std::cmp::min(remaining, v.capacity); + if size <= 0 { + continue; + } + + conn.execute( + "insert_transport", + &[ + &item.region_id, + &target_region, + &item.product_id, + &size, + &v.id, + ], + )?; + + remaining -= size; + } + + let shipped = best_quantity - remaining.max(0); + + // Optional: Logging zur Nachvollziehbarkeit + if shipped > 0 { + eprintln!( + "[DirectorWorker] Transport geplant: {} Einheiten von Produkt {} von Region {} nach Region {} (Stückpreis lokal {:.2}, remote {:.2})", + shipped, item.product_id, item.region_id, target_region, local_piece_price, best_remote_piece_price + ); + } + + Ok(shipped) + } + + fn get_transport_vehicles_for_route( + conn: &mut DbConnection, + falukant_user_id: i32, + source_region: i32, + target_region: i32, + ) -> Result, DbError> { + conn.prepare( + "get_transport_vehicles_for_route", + QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE, + )?; + let rows = conn.execute( + "get_transport_vehicles_for_route", + &[&falukant_user_id, &source_region, &target_region], + )?; + + let mut result = Vec::with_capacity(rows.len()); + for row in rows { + let id = row + .get("vehicle_id") + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1); + let capacity = row + .get("capacity") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + if id >= 0 && capacity > 0 { + result.push(TransportVehicle { id, capacity }); + } + } + + Ok(result) + } + + fn update_inventory_quantity( + conn: &mut DbConnection, + inventory_id: i32, + new_quantity: i32, + ) -> Result<(), DbError> { + const QUERY_UPDATE_INVENTORY_QTY: &str = r#" + UPDATE falukant_data.inventory + SET quantity = $2 + WHERE id = $1; + "#; + + conn.prepare("update_inventory_qty", QUERY_UPDATE_INVENTORY_QTY)?; + conn.execute("update_inventory_qty", &[&inventory_id, &new_quantity])?; + + Ok(()) + } + fn pay_salary(&mut self) -> Result<(), DbError> { self.base.set_current_step("DirectorWorker: pay_salary");