diff --git a/crates/ledger-mcp/src/server.rs b/crates/ledger-mcp/src/server.rs index d593883..e8c4ede 100644 --- a/crates/ledger-mcp/src/server.rs +++ b/crates/ledger-mcp/src/server.rs @@ -204,8 +204,14 @@ fn resolve_class(store: &Store, class: &str) -> Result { }) } +/// A money amount as a decimal string with at least two places: "0.00", +/// "207.68", "1000.00". Padding only, never rounding: an exact amount with +/// more places (an FX-converted line) keeps them. Never "-0". fn money(n: Decimal) -> String { - // Decimal's Display never emits floats or "-0" for exact values. + let mut n = if n.is_zero() { Decimal::ZERO } else { n }; + if n.scale() < 2 { + n.rescale(2); + } n.to_string() } @@ -2999,3 +3005,21 @@ impl ServerHandler for BooksServer { ))) } } + +#[cfg(test)] +mod tests { + use super::money; + use rust_decimal::Decimal; + use std::str::FromStr; + + #[test] + fn money_always_has_cents_and_never_rounds() { + let m = |s: &str| money(Decimal::from_str(s).unwrap()); + assert_eq!(money(Decimal::ZERO), "0.00"); + assert_eq!(m("-0"), "0.00"); + assert_eq!(m("1000"), "1000.00"); + assert_eq!(m("5.8"), "5.80"); + assert_eq!(m("-207.68"), "-207.68"); + assert_eq!(m("12.3456"), "12.3456"); + } +} diff --git a/crates/ledger-mcp/tests/mcp_stdio.rs b/crates/ledger-mcp/tests/mcp_stdio.rs index 5d70f07..3154c28 100644 --- a/crates/ledger-mcp/tests/mcp_stdio.rs +++ b/crates/ledger-mcp/tests/mcp_stdio.rs @@ -201,7 +201,7 @@ fn mcp_end_to_end_over_stdio() { .iter() .find(|l| l["code"] == "6000") .unwrap(); - assert_eq!(rent_line["balance_before"], "0"); + assert_eq!(rent_line["balance_before"], "0.00"); assert_eq!(rent_line["balance_after"], "100.00"); // Confirmed post without a receipt is rejected: >$25, no receipt. @@ -275,7 +275,7 @@ fn mcp_end_to_end_over_stdio() { assert_eq!(rev["status"], "posted"); let tb2 = mcp.call("books.trial_balance", json!({})); assert_eq!(tb2["balanced"], true); - assert_eq!(tb2["total_debits"], "0"); + assert_eq!(tb2["total_debits"], "0.00"); } #[test] @@ -381,7 +381,7 @@ fn mcp_assets_end_to_end_over_stdio() { assert_eq!(preview["status"], "preview"); assert_eq!(preview["book_value"], "58451.61"); assert_eq!(preview["gain_loss"], "1548.39"); - assert_eq!(preview["final_depreciation"], "0"); + assert_eq!(preview["final_depreciation"], "0.00"); assert_eq!(preview["receipt_required"], true); // Confirmed without the bill of sale: rejected.