use crate::account_balance::AccountBalance;
use crate::balance::Balance;
use crate::simplified_ledger::Error;
use chrono::NaiveDate;
use ledger_parser::{
Amount, Commodity, CommodityPosition, CommodityPrice, Posting, PostingAmount, PostingMetadata,
Reality, Transaction,
};
use ledger_parser::{Balance::Amount as BalanceAmount, Balance::Zero as BalanceZero};
use rust_decimal::Decimal;
pub fn calculate_amounts_from_balances(
transactions: &mut Vec<Transaction>,
commodity_prices: &mut Vec<CommodityPrice>,
) -> Result<(), Error> {
let mut running_balance = Some(Balance::new());
for transaction in transactions {
calculate_transaction_amounts(transaction, commodity_prices, &mut running_balance)?;
}
Ok(())
}
pub fn calculate_omitted_amounts(transaction: &mut Transaction) -> Result<(), Error> {
let mut commodity_prices = Vec::new();
calculate_transaction_amounts(transaction, &mut commodity_prices, &mut None)
}
fn calculate_transaction_amounts(
transaction: &mut Transaction,
commodity_prices: &mut Vec<CommodityPrice>,
running_balance: &mut Option<Balance>,
) -> Result<(), Error> {
let original_transaction = transaction.clone();
let mut real_transaction_balance = AccountBalance::new();
let mut virtual_transaction_balance = AccountBalance::new();
for posting in &transaction.postings {
if let Some(PostingAmount { ref amount, .. }) = posting.amount {
match posting.reality {
Reality::Real => real_transaction_balance += amount,
Reality::BalancedVirtual => virtual_transaction_balance += amount,
Reality::UnbalancedVirtual => (),
}
if let Some(running_balance) = running_balance {
running_balance.add_amount(&posting.account, amount);
}
}
}
if let Some(running_balance) = running_balance {
for posting in &mut transaction.postings {
if posting.balance.is_some() {
calculate_posting_amounts_from_balances(
&original_transaction,
posting,
running_balance,
&mut real_transaction_balance,
&mut virtual_transaction_balance,
)?;
}
}
}
transaction.postings.retain(|posting| {
posting
.amount
.as_ref()
.map_or(true, |amt| !amt.amount.commodity.name.is_empty())
});
let mut new_postings = Vec::new();
for posting in &transaction.postings {
if posting.amount.is_none() {
let balancing_postings = calculate_omitted_amounts_for_posting(
&original_transaction,
posting,
&mut real_transaction_balance,
&mut virtual_transaction_balance,
)?;
if let Some(running_balance) = running_balance {
for balancing_posting in &balancing_postings {
running_balance.add_amount(
&balancing_posting.account,
&balancing_posting
.amount
.as_ref()
.expect("we just calculated all the amounts")
.amount,
);
}
}
new_postings.extend(balancing_postings);
} else {
new_postings.push(posting.clone());
}
}
if !real_transaction_balance.is_zero()
&& !handle_commodity_exchange(
transaction.date,
&real_transaction_balance,
commodity_prices,
)
|| !virtual_transaction_balance.is_zero()
&& !handle_commodity_exchange(
transaction.date,
&virtual_transaction_balance,
commodity_prices,
)
{
return Err(Error::UnbalancedTransaction(transaction.clone().into()));
}
transaction.postings = new_postings;
Ok(())
}
fn handle_commodity_exchange(
transaction_date: NaiveDate,
balance: &AccountBalance,
commodity_prices: &mut Vec<CommodityPrice>,
) -> bool {
if let Some(commodity_price) = get_commodity_price_from_balance(transaction_date, balance) {
commodity_prices.push(commodity_price);
true
} else {
false
}
}
fn get_commodity_price_from_balance(
transaction_date: NaiveDate,
balance: &AccountBalance,
) -> Option<CommodityPrice> {
if balance.amounts.len() == 2 {
let amounts: Vec<_> = balance.amounts.iter().map(|f| f.1).collect();
if amounts[0].quantity != Decimal::new(0, 0) && amounts[1].quantity != Decimal::new(0, 0) {
return Some(CommodityPrice {
datetime: transaction_date.and_hms_opt(0, 0, 0).unwrap(),
commodity_name: amounts[0].commodity.name.clone(),
amount: Amount {
quantity: -amounts[1].quantity / amounts[0].quantity,
commodity: amounts[1].commodity.clone(),
},
});
}
}
None
}
fn calculate_posting_amounts_from_balances(
transaction: &Transaction,
posting: &mut Posting,
running_balance: &mut Balance,
real_transaction_balance: &mut AccountBalance,
virtual_transaction_balance: &mut AccountBalance,
) -> Result<(), Error> {
if let Some(posting_balance) = &posting.balance {
let account_balance = running_balance.account_balances.get(&posting.account);
let (commodity, balance_target, current_balance) = match posting_balance {
BalanceAmount(balance_amount) => {
match account_balance.and_then(|b| b.amounts.get(&balance_amount.commodity.name)) {
Some(current_balance) => {
(
balance_amount.commodity.clone(),
balance_amount.quantity,
current_balance.quantity,
)
}
None => {
(
balance_amount.commodity.clone(),
balance_amount.quantity,
Decimal::ZERO,
)
}
}
}
BalanceZero => match account_balance.map_or(0, |b| b.amounts.len()) {
0 => {
(
Commodity {
name: "".to_owned(),
position: CommodityPosition::Left,
},
Decimal::ZERO,
Decimal::ZERO,
)
}
1 => {
let current_balance = account_balance
.expect("just checked there's an account")
.amounts
.values()
.next()
.expect("just checked there's one commodity");
(
current_balance.commodity.clone(),
Decimal::ZERO,
current_balance.quantity,
)
}
_ => {
return Err(Error::ZeroBalanceMultipleCurrencies(
transaction.clone().into(),
));
}
},
};
if posting.amount.is_some() {
if let BalanceAmount(posting_balance) = posting_balance {
if posting_balance.quantity != current_balance {
return Err(Error::BalanceAssertionFailed(transaction.clone().into()));
}
} else if current_balance != Decimal::ZERO {
return Err(Error::ZeroBalanceAssertionFailed(
transaction.clone().into(),
));
}
} else {
let amount = Amount {
commodity,
quantity: balance_target - current_balance,
};
match posting.reality {
Reality::Real => *real_transaction_balance += &amount,
Reality::BalancedVirtual => *virtual_transaction_balance += &amount,
Reality::UnbalancedVirtual => (),
}
running_balance.add_amount(&posting.account, &amount);
posting.amount = Some(PostingAmount {
amount,
lot_price: None,
price: None,
});
}
posting.balance = None;
}
Ok(())
}
fn calculate_omitted_amounts_for_posting(
transaction: &Transaction,
posting: &Posting,
real_transaction_balance: &mut AccountBalance,
virtual_transaction_balance: &mut AccountBalance,
) -> Result<Vec<Posting>, Error> {
let transaction_balance = match posting.reality {
Reality::Real => real_transaction_balance,
Reality::BalancedVirtual => virtual_transaction_balance,
Reality::UnbalancedVirtual => {
return Err(Error::UnbalancedVirtualWithNoAmount(
transaction.clone().into(),
))
}
};
let mut sorted_amounts: Vec<_> = transaction_balance.amounts.values().collect();
sorted_amounts.sort_by_key(|amount| &amount.commodity.name);
let new_postings = sorted_amounts
.iter()
.map(|amount| {
let amount = Amount {
commodity: amount.commodity.clone(),
quantity: -amount.quantity,
};
Posting {
account: posting.account.clone(),
reality: posting.reality,
status: posting.status,
comment: posting.comment.clone(),
metadata: PostingMetadata {
date: None,
effective_date: None,
tags: vec![],
},
amount: Some(PostingAmount {
amount,
lot_price: None,
price: None,
}),
balance: None,
}
})
.collect();
*transaction_balance = AccountBalance::new();
Ok(new_postings)
}
#[cfg(test)]
mod tests {
use super::*;
use ledger_parser::{Ledger, LedgerItem};
fn parse_transaction(input: &str) -> Transaction {
input
.parse::<Ledger>()
.unwrap()
.items
.into_iter()
.find_map(|item| match item {
LedgerItem::Transaction(txn) => Some(txn),
_ => None,
})
.unwrap()
}
fn parse_transactions(input: &str) -> Vec<Transaction> {
input
.parse::<Ledger>()
.unwrap()
.items
.into_iter()
.filter_map(|item| match item {
LedgerItem::Transaction(txn) => Some(txn),
_ => None,
})
.collect()
}
#[test]
fn test_calculate_omitted_amounts_no_changes() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
"#,
);
let original_transaction = transaction.clone();
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_single_commodity() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF ; Comment
"#,
);
let expected_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20 ; Comment
"#,
);
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, expected_transaction);
}
#[test]
fn test_calculate_omitted_amounts_multi_commodity() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF £3.40
TEST:GHI ; Comment
"#,
);
let expected_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF £3.40
TEST:GHI $-1.20 ; Comment
TEST:GHI £-3.40 ; Comment
"#,
);
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, expected_transaction);
}
#[test]
fn test_calculate_omitted_amounts_error_unbalanced_transaction() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.21
"#,
);
let original_transaction = transaction.clone();
assert_eq!(
calculate_omitted_amounts(&mut transaction),
Err(Error::UnbalancedTransaction(
original_transaction.clone().into()
))
);
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_virtual_no_changes() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] $-3.40
"#,
);
let original_transaction = transaction.clone();
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_virtual_single_commodity() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF ; Comment 1
[TEST:GHI] $3.40
[TEST:JKL] ; Comment 2
"#,
);
let expected_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20 ; Comment 1
[TEST:GHI] $3.40
[TEST:JKL] $-3.40 ; Comment 2
"#,
);
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, expected_transaction);
}
#[test]
fn test_calculate_omitted_amounts_virtual_multi_commodity() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] £5.60
[TEST:MNO] ; Comment
"#,
);
let expected_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] £5.60
[TEST:MNO] $-3.40 ; Comment
[TEST:MNO] £-5.60 ; Comment
"#,
);
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, expected_transaction);
}
#[test]
fn test_calculate_omitted_amounts_virtual_error_unbalanced_transaction() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] $-3.45
"#,
);
let original_transaction = transaction.clone();
assert_eq!(
calculate_omitted_amounts(&mut transaction),
Err(Error::UnbalancedTransaction(
original_transaction.clone().into()
))
);
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_error_mixed_real_virtual_transaction() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
[TEST:ABC] $3.40
TEST:DEF
"#,
);
let original_transaction = transaction.clone();
assert_eq!(
calculate_omitted_amounts(&mut transaction),
Err(Error::UnbalancedTransaction(
original_transaction.clone().into()
))
);
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_unbalanced_virtual_no_changes() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] $-3.40
(TEST:MNO) $5.60 ; Unbalanced virtual posting
"#,
);
let original_transaction = transaction.clone();
assert_eq!(calculate_omitted_amounts(&mut transaction), Ok(()));
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_omitted_amounts_error_unbalanced_virtual_posting_without_amount() {
let mut transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
(TEST:GHI)
"#,
);
let original_transaction = transaction.clone();
assert_eq!(
calculate_omitted_amounts(&mut transaction),
Err(Error::UnbalancedVirtualWithNoAmount(
original_transaction.clone().into()
))
);
assert_eq!(transaction, original_transaction);
}
#[test]
fn test_calculate_amounts_from_balances_no_change() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $3.40
TEST:DEF $-3.40
"#,
);
let original_transactions = transactions.clone();
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, original_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_single_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC = $4.60 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $3.40 ; Comment
TEST:DEF $-3.40
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_multi_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:ABC £3.40
TEST:DEF $-1.20
TEST:DEF £-3.40
2018-10-01 Marek Ogarek
TEST:ABC = £5.60 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:ABC £3.40
TEST:DEF $-1.20
TEST:DEF £-3.40
2018-10-01 Marek Ogarek
TEST:ABC £2.20 ; Comment
TEST:DEF £-2.20
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_of_unbalanced_virtual() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
[TEST:ABC] $3.40
[TEST:DEF]
2018-10-01 Marek Ogarek
(TEST:ABC) = $5.60
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:ABC] $3.40
[TEST:DEF] $-3.40
2018-10-01 Marek Ogarek
(TEST:ABC) $1.00
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_zero_balance_single_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC = 0 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $-1.20 ; Comment
TEST:DEF $1.20
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_error_zero_balance_multi_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:ABC £3.40
TEST:DEF $-1.20
TEST:DEF £-3.40
2018-10-01 Marek Ogarek
TEST:ABC = 0 ; Comment
TEST:DEF
"#,
);
let error_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC = 0 ; Comment
TEST:DEF
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::ZeroBalanceMultipleCurrencies(
error_transaction.into()
))
);
}
#[test]
fn test_calculate_amounts_from_balances_error_unbalanced_transaction() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $1.21
TEST:DEF = $0.00
"#,
);
let error_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.21
TEST:DEF $1.20
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::UnbalancedTransaction(error_transaction.into()))
);
}
#[test]
fn test_calculate_amounts_from_balances_virtual_no_change() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:GHI] $3.40
[TEST:JKL] $-3.40
2018-10-01 Marek Ogarek
TEST:ABC $5.60
TEST:DEF $-5.60
[TEST:GHI] $7.80
[TEST:JKL] $-7.80
"#,
);
let original_transactions = transactions.clone();
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, original_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_virtual_single_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
[TEST:ABC] $3.40
[TEST:DEF]
2018-10-01 Marek Ogarek
[TEST:ABC] = $5.60
[TEST:DEF]
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:ABC] $3.40
[TEST:DEF] $-3.40
2018-10-01 Marek Ogarek
[TEST:ABC] $1.00
[TEST:DEF] $-1.00
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_virtual_balancing_transaction() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
[TEST:ABC] = $1.20
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
[TEST:ABC] $0.00
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_error_virtual_balancing_transaction_fails() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
[TEST:ABC] = $1.21
"#,
);
let error_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
[TEST:ABC] $0.01
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::UnbalancedTransaction(error_transaction.into()))
);
}
#[test]
fn test_calculate_amounts_from_balances_virtual_multi_commodity() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
[TEST:ABC] £3.40
TEST:DEF $-1.20
[TEST:DEF] £-3.40
2018-10-01 Marek Ogarek
TEST:ABC = £5.60 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
[TEST:ABC] £3.40
TEST:DEF $-1.20
[TEST:DEF] £-3.40
2018-10-01 Marek Ogarek
TEST:ABC £2.20 ; Comment
TEST:DEF £-2.20
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_virtual_error_unbalanced_transaction() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
[TEST:ABC] $3.40
TEST:DEF $-1.20
[TEST:DEF] $-3.40
2018-10-01 Marek Ogarek
TEST:ABC $-1.20
TEST:DEF = $0.00
"#,
);
let error_transaction = parse_transaction(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $-1.20
TEST:DEF $4.60
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::UnbalancedTransaction(error_transaction.into()))
);
}
#[test]
fn test_calculate_amounts_from_balances_with_unbalanced_virtual() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
[TEST:ABC] $3.40
[TEST:DEF]
(TEST:ABC) $5.60
2018-10-01 Marek Ogarek
TEST:ABC = $11.20
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
[TEST:ABC] $3.40
[TEST:DEF] $-3.40
(TEST:ABC) $5.60
2018-10-01 Marek Ogarek
TEST:ABC $1.00
TEST:DEF $-1.00
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_empty_account() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $-1.20 ; Account is empty
TEST:DEF $1.20
2018-10-01 Marek Ogarek
TEST:ABC = $3.40 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $-1.20 ; Account is empty
TEST:DEF $1.20
2018-10-01 Marek Ogarek
TEST:ABC $3.40 ; Comment
TEST:DEF $-3.40
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_unknown_account() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC = $3.40 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $3.40 ; Comment
TEST:DEF $-3.40
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_balance_assertion() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
TEST:ABC $3.40 = $4.60 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $3.40 ; Comment
TEST:DEF $-3.40
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_error_balance_assertion_fails() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
TEST:ABC $3.41 = $4.60 ; Comment
TEST:DEF
"#,
);
let error_transaction = transactions[1].clone();
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::BalanceAssertionFailed(error_transaction.into()))
);
}
#[test]
fn test_calculate_amounts_from_balances_zero_balance_assertion() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
TEST:ABC $-1.20 = 0 ; Comment
TEST:DEF
"#,
);
let expected_transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF $-1.20
2018-10-01 Marek Ogarek
TEST:ABC $-1.20 ; Comment
TEST:DEF $1.20
"#,
);
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Ok(())
);
assert_eq!(transactions, expected_transactions);
}
#[test]
fn test_calculate_amounts_from_balances_error_zero_balance_assertion_fails() {
let mut transactions = parse_transactions(
r#"
2018-10-01 Marek Ogarek
TEST:ABC $1.20
TEST:DEF
2018-10-01 Marek Ogarek
TEST:ABC $-1.21 = 0 ; Comment
TEST:DEF
"#,
);
let error_transaction = transactions[1].clone();
assert_eq!(
calculate_amounts_from_balances(&mut transactions, &mut Vec::new()),
Err(Error::ZeroBalanceAssertionFailed(error_transaction.into()))
);
}
}