Enhance change_falukant_user_money: Implement money clamping logic to ensure updates stay within numeric(10,2) limits, improving data integrity and error handling.

This commit is contained in:
Torsten Schulz (local)
2025-12-08 16:34:57 +01:00
parent 99fbaab816
commit 180431b130

View File

@@ -158,46 +158,62 @@ impl BaseWorker {
))); )));
} }
// We must ensure the resulting money fits in numeric(10,2).
// numeric(10,2) max absolute value is < 10^8 (100_000_000) before rounding.
// Fetch current money for the user and clamp the delta if needed.
const QUERY_GET_MONEY: &str = r#"
SELECT money FROM falukant_data.falukant_user WHERE id = $1;
"#;
conn.prepare("get_money_for_clamp", QUERY_GET_MONEY)?;
let rows = conn.execute("get_money_for_clamp", &[&falukant_user_id])?;
let current_money: f64 = rows
.get(0)
.and_then(|r| r.get("money"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
// compute tentative result
let tentative = current_money + money_change;
// numeric(10,2) allows values with absolute < 10^8 (100_000_000)
const MAX_ABS: f64 = 100_000_000.0 - 0.01; // leave room for scale
let adjusted_money_change = if tentative >= MAX_ABS {
let clipped = MAX_ABS - current_money;
eprintln!(
"[BaseWorker] Clamping money_change: tentative {} exceeds numeric(10,2) max, clipping to {}",
tentative, clipped
);
clipped
} else if tentative <= -MAX_ABS {
let clipped = -MAX_ABS - current_money;
eprintln!(
"[BaseWorker] Clamping money_change: tentative {} below min, clipping to {}",
tentative, clipped
);
clipped
} else {
money_change
};
// Send exact types matching the DB function signature: // Send exact types matching the DB function signature:
// p_falukant_user_id integer, p_money_change numeric, p_activity text
let uid_i32: i32 = falukant_user_id; let uid_i32: i32 = falukant_user_id;
let money_str = money_change.to_string(); // numeric accepts text let money_str = format!("{:.2}", adjusted_money_change);
let p1: &(dyn ToSql + Sync) = &uid_i32; let p1: &(dyn ToSql + Sync) = &uid_i32;
let p2: &(dyn ToSql + Sync) = &money_str; let p2: &(dyn ToSql + Sync) = &money_str;
let p3: &(dyn ToSql + Sync) = &action; let p3: &(dyn ToSql + Sync) = &action;
// Minimal logging
eprintln!( eprintln!(
"[BaseWorker] change_falukant_user_money: update_money(user_id={}, money_change='{}', action={})", "[BaseWorker] change_falukant_user_money: update_money(user_id={}, money_change='{}', action={})",
uid_i32, money_str, action uid_i32, money_str, action
); );
// Try parameterized call first // Execute parameterized
match conn.execute("update_money", &[p1, p2, p3]) { let _ = conn.execute("update_money", &[p1, p2, p3])?;
Ok(_) => return Ok(()),
Err(err) => {
eprintln!(
"[BaseWorker] parameterized update_money failed: {err}, falling back to literal SQL",
);
// Fall back: build SQL with literals. Escape action safely (double single-quotes).
fn escape_sql_literal(s: &str) -> String {
s.replace('\'', "''")
}
let escaped_action = escape_sql_literal(action); Ok(())
// money_str is already a numeric literal string (e.g. "3726" or "1597.12")
let sql = format!(
"SELECT falukant_data.update_money({}, {}::numeric, '{}');",
uid_i32, money_str, escaped_action
);
// Use query without parameters
let _ = conn.query(&sql)?;
return Ok(());
}
}
} }
} }