From cd21293fbdc631e7499ce546e2377b9109229d1c Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 13 Apr 2026 15:29:11 +0200 Subject: [PATCH] Enhance DirectorWorker logic for tax calculations: Updated the `DirectorWorker` to compute the cumulative tax percentage for net ranking and adjusted SQL queries to include this new parameter in production selection. Improved logging to capture additional metrics related to tax and net piece calculations, enhancing the traceability of production decisions. --- src/worker/director.rs | 23 +++- src/worker/sql.rs | 276 ++++++++++++++++++++--------------------- 2 files changed, 158 insertions(+), 141 deletions(-) diff --git a/src/worker/director.rs b/src/worker/director.rs index 1e33269..3a6d14b 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -208,9 +208,20 @@ impl DirectorWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + // Initial: Kumulativen Steuerprozentsatz für Netto-Ranking ermitteln. + conn.prepare("get_director_user", QUERY_GET_DIRECTOR_USER)?; + let user_rows = conn.execute("get_director_user", &[&director.id])?; + let falukant_user_id = user_rows + .first() + .and_then(|row| row.get("falukant_user_id")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_TREASURY_USER_ID); + let tax_percent = Self::get_cumulative_tax_percent(&mut conn, director.branch_id, falukant_user_id) + .unwrap_or(DEFAULT_TAX_PERCENT); + // Initial: Bestes Produkt für diesen Branch ermitteln conn.prepare("get_to_produce_debug", QUERY_GET_BEST_PRODUCTION_DEBUG)?; - let debug_rows = conn.execute("get_to_produce_debug", &[&director.id, &director.branch_id])?; + let debug_rows = conn.execute("get_to_produce_debug", &[&director.id, &director.branch_id, &tax_percent])?; for (idx, row) in debug_rows.iter().enumerate() { let rank = idx + 1; let product_id = row.get("product_id").map(|s| s.as_str()).unwrap_or("?"); @@ -224,9 +235,12 @@ impl DirectorWorker { let effective_percent = row.get("effective_percent").map(|s| s.as_str()).unwrap_or("?"); let one_piece_cost = row.get("one_piece_cost").map(|s| s.as_str()).unwrap_or("?"); let revenue_piece = row.get("revenue_piece").map(|s| s.as_str()).unwrap_or("?"); + let tax_piece = row.get("tax_piece").map(|s| s.as_str()).unwrap_or("?"); + let net_piece = row.get("net_piece").map(|s| s.as_str()).unwrap_or("?"); + let row_tax_percent = row.get("tax_percent").map(|s| s.as_str()).unwrap_or("?"); let worth = row.get("worth").map(|s| s.as_str()).unwrap_or("?"); eprintln!( - "[DirectorWorker][best_production_debug] director_id={} branch_id={} rank={} product_id={} label={} cat={} prod_time={} sell_cost={} market%={} knowledge(char={},dir={}) effective%={} one_piece_cost={} revenue_piece={} worth={}", + "[DirectorWorker][best_production_debug] director_id={} branch_id={} rank={} product_id={} label={} cat={} prod_time={} sell_cost={} market%={} knowledge(char={},dir={}) effective%={} one_piece_cost={} revenue_piece={} tax_piece={} net_piece={} tax%={} worth={}", director.id, director.branch_id, rank, @@ -241,12 +255,15 @@ impl DirectorWorker { effective_percent, one_piece_cost, revenue_piece, + tax_piece, + net_piece, + row_tax_percent, worth ); } conn.prepare("get_to_produce", QUERY_GET_BEST_PRODUCTION)?; - let rows = conn.execute("get_to_produce", &[&director.id, &director.branch_id])?; + let rows = conn.execute("get_to_produce", &[&director.id, &director.branch_id, &tax_percent])?; if rows.is_empty() { return Ok(()); } diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 5841bdd..57c11bc 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -73,12 +73,14 @@ JOIN falukant_data.branch b ON b.region_id = c.region_id AND b.falukant_user_id WHERE current_time BETWEEN '08:00:00' AND '17:00:00'; "#; -/// Bester Produktstart für den Direktor: -/// Wissen geht additiv in den Markt-Prozentsatz `(worth + (2×Char + Dir)/3) / 100`. -/// Für das Ranking werden dieselben Stückkosten wie im DirectorWorker angesetzt: -/// `raw = 6 + category`, mit Headroom-Rabatt über Zertifikat (`min((cert-cat)*0.035, 0.14)`). -/// Skalierung: `/ (300 × production_time)`. -/// Verkauf/Steuer im laufenden Spiel nutzt weiterhin `DirectorWorker::compute_piece_sell_price` — hier nur Ranking. +/// Bester Produktstart für den Direktor nach erwartbarem **Netto-Gewinn pro Minute**. +/// Erwarteter Erlös/Stück nutzt dieselbe 60%-100%-Bandlogik wie beim Verkauf: +/// `base = sell_cost * market_percent / 100` +/// `quality = clamp((2*knowledge_char + knowledge_dir)/3, 0..100)` +/// `revenue_piece = base * (0.6 + 0.4 * quality/100)` +/// Stückkosten wie im DirectorWorker (`6 + category`, Headroom-Rabatt). +/// Steuerabzug auf Profitanteil über `$3` (kumulativer Steuerprozentsatz für die Branch-Region). +/// Rankinggröße `worth` = `net_piece / production_time`. /// /// `worth_percent`: mit Fahrzeug `MAX` über Filialregionen; ohne Fahrzeug nur Direktor-Region. /// **Ein** Spielercharakter je User (`ORDER BY id DESC LIMIT 1`, `health > 0`). @@ -89,40 +91,96 @@ COALESCE(ftp.category, 1)::int AS product_category, fdb.region_id, 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 - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + ( - 2.0 * COALESCE(fdk_character.knowledge, 0)::float8 - + COALESCE(fdk_director.knowledge, 0)::float8 - ) - / 3.0 - ) - / 100.0 + ( + ( + ftp.sell_cost + * ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id + ) + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) / 100.0 + ) * ( + 0.6 + 0.4 * ( + LEAST( + 100.0, + GREATEST( + 0.0, + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 + ) + ) / 100.0 + ) + ) + ) - ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) - ) - ) - ) / (300.0 * NULLIF(ftp.production_time::float8, 0.0)) + GREATEST( + ( + ( + ( + ( + ftp.sell_cost + * ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id + ) + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) / 100.0 + ) * ( + 0.6 + 0.4 * ( + LEAST( + 100.0, + GREATEST( + 0.0, + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 + ) + ) / 100.0 + ) + ) + ) + - ( + ( + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 + ) + ) + ) + ), + 0.0 + ) * (COALESCE($3::float8, 0.0) / 100.0) + ) + + ( + ( + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 + ) + ) + ) + ) + ) / NULLIF(ftp.production_time::float8, 0.0) ) 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 @@ -162,114 +220,39 @@ WHERE fdd.id = $1 AND fdb.id = $2 ORDER BY worth DESC LIMIT 1; /// Debug-Variante zur Nachvollziehbarkeit der Direktoren-Entscheidung. /// Liefert die Top-5 Kandidaten inkl. Teilmetriken, die in `worth` einfließen. pub const QUERY_GET_BEST_PRODUCTION_DEBUG: &str = r#" +WITH candidates AS ( SELECT ftp.id::text AS product_id, ftp.label_tr, COALESCE(ftp.category, 1)::int::text AS product_category, COALESCE(ftp.production_time, 0)::int::text AS production_time, ROUND(ftp.sell_cost::numeric, 2)::text AS sell_cost, - ROUND( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - )::numeric, - 2 - )::text AS market_percent, - COALESCE(fdk_character.knowledge, 0)::int::text AS knowledge_character, - COALESCE(fdk_director.knowledge, 0)::int::text AS knowledge_director, - ROUND( - ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END + ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - )::numeric, - 2 - )::text AS effective_percent, - ROUND( + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) AS market_percent_val, + COALESCE(fdk_character.knowledge, 0)::float8 AS knowledge_char_val, + COALESCE(fdk_director.knowledge, 0)::float8 AS knowledge_dir_val, + ( ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 ) - )::numeric, - 4 - )::text AS one_piece_cost, - ROUND( - ( - ftp.sell_cost - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - ) / 100.0 - )::numeric, - 4 - )::text AS revenue_piece, - ROUND( - ( - ( - ftp.sell_cost - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - ) / 100.0 - - ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) - ) - ) - ) / (300.0 * NULLIF(ftp.production_time::float8, 0.0)) - )::numeric, - 8 - )::text AS worth + ) + ) AS one_piece_cost_val 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 @@ -301,6 +284,23 @@ LEFT JOIN falukant_data.town_product_worth fdtpw_local JOIN falukant_data.knowledge fdk_character ON fdk_character.product_id = ftp.id AND fdk_character.character_id = user_character.id JOIN falukant_data.knowledge fdk_director ON fdk_director.product_id = ftp.id AND fdk_director.character_id = fdd.director_character_id WHERE fdd.id = $1 AND fdb.id = $2 +) +SELECT + product_id, + label_tr, + product_category, + production_time, + sell_cost, + ROUND(market_percent_val::numeric, 2)::text AS market_percent, + ROUND(knowledge_char_val::numeric, 0)::int::text AS knowledge_character, + ROUND(knowledge_dir_val::numeric, 0)::int::text AS knowledge_director, + ROUND(LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0))::numeric, 2)::text AS effective_percent, + ROUND(one_piece_cost_val::numeric, 4)::text AS one_piece_cost, + ROUND((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)))::numeric, 4)::text AS revenue_piece, + ROUND((GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0))::numeric, 4)::text AS tax_piece, + ROUND(((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0))) - (GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0)) - one_piece_cost_val)::numeric, 4)::text AS net_piece, + ROUND(COALESCE($3::numeric, 0), 2)::text AS tax_percent, + ROUND((((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0))) - (GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0)) - one_piece_cost_val) / NULLIF(production_time::float8, 0.0))::numeric, 8)::text AS worth ORDER BY worth DESC LIMIT 5; "#;