mod common;
use rustledger_parser::parse;
fn balance_errors(src: &str) -> Vec<String> {
let parsed = parse(src);
assert!(
parsed.errors.is_empty(),
"parse errors: {:?}",
parsed.errors
);
let directives: Vec<_> = parsed.directives.into_iter().map(|d| d.value).collect();
common::validate(&directives)
.into_iter()
.map(|e| e.to_string())
.filter(|m| m.contains("balance"))
.collect()
}
const HEADER: &str = "2024-01-01 open Assets:B\n2024-01-01 open Assets:Cash\n\n";
#[test]
fn per_unit_form_balances() {
let errors = balance_errors(&format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{5.00 USD}}\n Assets:Cash -10.00 USD\n"
));
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn compound_zero_total_balances() {
let errors = balance_errors(&format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{5.00 # 0.00 USD}}\n Assets:Cash -10.00 USD\n"
));
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn compound_total_only_balances() {
let errors = balance_errors(&format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{# 10.00 USD}}\n Assets:Cash -10.00 USD\n"
));
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn compound_both_detects_imbalance() {
let errors = balance_errors(&format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{5.00 # 10.00 USD}}\n Assets:Cash -10.00 USD\n"
));
assert!(
!errors.is_empty(),
"an unbalanced compound-cost transaction must be rejected"
);
}
#[test]
fn compound_both_balances_at_full_weight() {
let errors = balance_errors(&format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{5.00 # 10.00 USD}}\n Assets:Cash -20.00 USD\n"
));
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn compound_books_combined_per_unit() {
use rustledger_core::{BookingMethod, Decimal};
let src = format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{6.00 # 10.00 USD}}\n Assets:Cash -22.00 USD\n"
);
let parsed = parse(&src);
let directives: Vec<_> = parsed.directives.into_iter().map(|d| d.value).collect();
let result = rustledger_booking::book(&directives, BookingMethod::Strict);
assert!(
result.failed.is_empty(),
"booking failures: {:?}",
result.failed
);
let txn = result
.booked
.iter()
.find_map(|d| match d {
rustledger_core::Directive::Transaction(t) => Some(t),
_ => None,
})
.expect("transaction present");
let cost = txn.postings[0]
.cost
.as_ref()
.and_then(|c| c.number)
.expect("booked cost number");
assert_eq!(
cost.per_unit(),
Some(Decimal::new(1100, 2)),
"booked per-unit must be (2*6 + 10)/2 = 11.00, got {cost:?}"
);
assert_eq!(
cost.total(),
Some(Decimal::new(2200, 2)),
"preserved total must be the combined 22.00, got {cost:?}"
);
}
#[test]
fn compound_reduction_matches_and_books() {
let src = format!(
"{HEADER}2024-01-10 * \"buy\"\n Assets:B 2 VTI {{6.00 # 10.00 USD}}\n Assets:Cash -22.00 USD\n\n\
2024-02-10 * \"sell by resolved per-unit\"\n Assets:B -2 VTI {{11.00 USD}}\n Assets:Cash 22.00 USD\n\n\
2024-03-10 * \"buy again\"\n Assets:B 2 VTI {{5.00 # 0.00 USD}}\n Assets:Cash -10.00 USD\n\n\
2024-04-10 * \"sell filtering by compound spec\"\n Assets:B -2 VTI {{5.00 # 0.00 USD}}\n Assets:Cash 10.00 USD\n"
);
let errors = balance_errors(&src);
assert!(errors.is_empty(), "{errors:?}");
}