Skip to content

Commit

Permalink
fix incomplete Action implementation for BalanceAssertion
Browse files Browse the repository at this point in the history
- fixed clippy warnings
- formatted code
- bumped version to v0.8.1
  • Loading branch information
kellpossible committed May 15, 2020
1 parent 9b7219c commit 0e9fecc
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 50 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v0.8.1

+ Fix incomplete `Action` implementation for `BalanceAssertion`.

## v0.8.0

+ Refactor to properly support user defined `Action`s. This included making `Program` and a number of other types generic over `ActionTypeValueEnum` and `ActionTypeEnum` implementations. These changes shouldn't affect anyone using the provided set of `Actions`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "doublecount"
version = "0.8.0"
version = "0.8.1"
description = "A double entry accounting system/library."
keywords = ["financial", "currency", "accounting", "exchange", "rate"]
categories = ["science", "mathematics"]
Expand Down
9 changes: 4 additions & 5 deletions src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,12 @@ impl Account {
) -> Account {
let id_string: String = nanoid!(ACCOUNT_ID_LENGTH);
Self::new(
ArrayString::from(id_string.as_ref()).expect(
format!(
ArrayString::from(id_string.as_ref()).unwrap_or_else(|_| {
panic!(
"generated id string {0} should fit within ACCOUNT_ID_LENGTH: {1}",
id_string, ACCOUNT_ID_LENGTH
)
.as_ref(),
),
}),
name,
commodity_type_id,
category,
Expand All @@ -79,7 +78,7 @@ impl Account {
id,
name: name.map(|s| s.into()),
commodity_type_id,
category: category.map(|c| c.into()),
category,
}
}
}
Expand Down
128 changes: 97 additions & 31 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,35 +381,29 @@ where

// Calculate the sum of elements (not including the empty element if there is one)
for (i, element) in self.elements.iter().enumerate() {
match empty_amount_element {
Some(empty_i) => {
if i != empty_i {
//TODO: perform commodity type conversion here if required
sum = match sum.add(&element.amount.as_ref().unwrap()) {
Ok(value) => value,
Err(error) => return Err(AccountingError::Commodity(error)),
}
if let Some(empty_i) = empty_amount_element {
if i != empty_i {
//TODO: perform commodity type conversion here if required
sum = match sum.add(&element.amount.as_ref().unwrap()) {
Ok(value) => value,
Err(error) => return Err(AccountingError::Commodity(error)),
}
}
None => {}
}
}

// Calculate the value to use for the empty element (negate the sum of the other elements)
match empty_amount_element {
Some(empty_i) => {
let modified_emtpy_element: &mut TransactionElement =
modified_elements.get_mut(empty_i).unwrap();
let negated_sum = sum.neg();
modified_emtpy_element.amount = Some(negated_sum.clone());

sum = match sum.add(&negated_sum) {
Ok(value) => value,
Err(error) => return Err(AccountingError::Commodity(error)),
}
if let Some(empty_i) = empty_amount_element {
let modified_emtpy_element: &mut TransactionElement =
modified_elements.get_mut(empty_i).unwrap();
let negated_sum = sum.neg();
modified_emtpy_element.amount = Some(negated_sum);

sum = match sum.add(&negated_sum) {
Ok(value) => value,
Err(error) => return Err(AccountingError::Commodity(error)),
}
None => {}
};
}

if sum.value != Decimal::zero() {
return Err(AccountingError::InvalidTransaction(
Expand All @@ -421,12 +415,11 @@ where
for transaction in &modified_elements {
let mut account_state = program_state
.get_account_state_mut(&transaction.account_id)
.expect(
format!(
.unwrap_or_else(||
panic!(
"unable to find state for account with id: {} please ensure this account was added to the program state before execution.",
transaction.account_id
)
.as_ref(),
);

match account_state.status {
Expand Down Expand Up @@ -459,7 +452,7 @@ where
}
}

return Ok(());
Ok(())
}
}

Expand Down Expand Up @@ -543,7 +536,7 @@ where
.get_account_state_mut(&self.account_id)
.unwrap();
account_state.status = self.newstatus;
return Ok(());
Ok(())
}
}

Expand Down Expand Up @@ -627,21 +620,27 @@ where
}

fn perform(&self, program_state: &mut ProgramState<AT, ATV>) -> Result<(), AccountingError> {
match program_state.get_account_state(&self.account_id) {
let failed_assertion = match program_state.get_account_state(&self.account_id) {
Some(state) => {
if state
if !state
.amount
.eq_approx(self.expected_balance, Commodity::default_epsilon())
{
Some(FailedBalanceAssertion::new(self.clone(), state.amount))
} else {
None
}
}
None => {
return Err(AccountingError::MissingAccountState(self.account_id));
}
};

if let Some(failed_assertion) = failed_assertion {
program_state.record_failed_balance_assertion(failed_assertion)
}

return Ok(());
Ok(())
}
}

Expand All @@ -654,7 +653,14 @@ impl ActionTypeFor<ActionType> for BalanceAssertion {
#[cfg(test)]
mod tests {
use super::ActionType;
use std::collections::HashSet;
use crate::{
Account, AccountStatus, AccountingError, ActionTypeValue, ActionTypeValueEnum,
BalanceAssertion, Program, ProgramState, Transaction,
};
use chrono::NaiveDate;
use commodity::{Commodity, CommodityType};
use rust_decimal::Decimal;
use std::{collections::HashSet, rc::Rc};

#[test]
fn action_type_order() {
Expand Down Expand Up @@ -690,6 +696,66 @@ mod tests {

assert_eq!(action_types_ordered, action_types_unordered);
}

#[test]
fn balance_assertion() {
let aud = Rc::from(CommodityType::from_currency_alpha3("AUD").unwrap());
let account1 = Rc::from(Account::new_with_id(Some("Account 1"), aud.id, None));
let account2 = Rc::from(Account::new_with_id(Some("Account 2"), aud.id, None));

let date_1 = NaiveDate::from_ymd(2020, 01, 01);
let date_2 = NaiveDate::from_ymd(2020, 01, 02);
let actions: Vec<Rc<ActionTypeValue>> = vec![
Rc::new(
Transaction::new_simple::<String>(
None,
date_1.clone(),
account1.id,
account2.id,
Commodity::new(Decimal::new(100, 2), &*aud),
None,
)
.into(),
),
// This assertion is expected to fail because it occurs at the start
// of the day (before the transaction).
Rc::new(
BalanceAssertion::new(
account2.id,
date_1.clone(),
Commodity::new(Decimal::new(100, 2), &*aud),
)
.into(),
),
// This assertion is expected to pass because it occurs at the end
// of the day (after the transaction).
Rc::new(
BalanceAssertion::new(
account2.id,
date_2.clone(),
Commodity::new(Decimal::new(100, 2), &*aud),
)
.into(),
),
];

let program = Program::new(actions);

let accounts = vec![account1, account2];
let mut program_state = ProgramState::new(&accounts, AccountStatus::Open);
match program_state.execute_program(&program) {
Err(AccountingError::BalanceAssertionFailed(failure)) => {
assert_eq!(
Commodity::new(Decimal::new(0, 2), &*aud),
failure.actual_balance
);
assert_eq!(date_1, failure.assertion.date);
}
_ => panic!("Expected an AccountingError:BalanceAssertionFailed"),
}

assert_eq!(1, program_state.failed_balance_assertions.len());
}
}

#[cfg(feature = "serde-support")]
Expand Down
25 changes: 12 additions & 13 deletions src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ where
pub fn len(&self) -> usize {
self.actions.len()
}

/// Returns true if there are no actions in this progam.
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
}

#[cfg(feature = "serde-support")]
Expand Down Expand Up @@ -146,7 +151,7 @@ pub fn sum_account_states(
) -> Result<Commodity, AccountingError> {
let mut sum = Commodity::zero(sum_commodity_type_id);

for (_, account_state) in account_states {
for account_state in account_states.values() {
let account_amount = if account_state.amount.type_id != sum_commodity_type_id {
match exchange_rate {
Some(rate) => rate.convert(account_state.amount, sum_commodity_type_id)?,
Expand All @@ -172,10 +177,7 @@ where
ATV: ActionTypeValueEnum<AT>,
{
/// Create a new [ProgramState](ProgramState).
pub fn new(
accounts: &Vec<Rc<Account>>,
account_status: AccountStatus,
) -> ProgramState<AT, ATV> {
pub fn new(accounts: &[Rc<Account>], account_status: AccountStatus) -> ProgramState<AT, ATV> {
let mut account_states = HashMap::new();

for account in accounts {
Expand Down Expand Up @@ -206,14 +208,11 @@ where
}

// TODO: change this to return a list of failed assertions in the error
match self.failed_balance_assertions.get(0) {
Some(failed_assertion) => {
return Err(AccountingError::BalanceAssertionFailed(
failed_assertion.clone(),
));
}
None => {}
};
if let Some(failed_assertion) = self.failed_balance_assertions.get(0) {
return Err(AccountingError::BalanceAssertionFailed(
failed_assertion.clone(),
));
}

Ok(())
}
Expand Down

0 comments on commit 0e9fecc

Please sign in to comment.