use super::{Account, AccountStatus, AccountingError, ProgramState};
use chrono::NaiveDate;
use commodity::exchange_rate::ExchangeRate;
use commodity::Commodity;
use rust_decimal::{prelude::Zero, Decimal};
use std::fmt;
use std::rc::Rc;
use std::slice;
#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Hash, Clone)]
pub enum ActionType {
EditAccountStatus,
BalanceAssertion,
Transaction,
}
impl ActionType {
pub fn iterator() -> slice::Iter<'static, ActionType> {
static ACTION_TYPES: [ActionType; 3] = [
ActionType::EditAccountStatus,
ActionType::BalanceAssertion,
ActionType::Transaction,
];
ACTION_TYPES.iter()
}
}
pub trait Action: fmt::Display + fmt::Debug {
fn date(&self) -> NaiveDate;
fn perform(&self, program_state: &mut ProgramState) -> Result<(), AccountingError>;
fn action_type(&self) -> ActionType;
}
pub struct ActionOrder(pub Rc<dyn Action>);
impl PartialEq for ActionOrder {
fn eq(&self, other: &ActionOrder) -> bool {
self.0.action_type() == other.0.action_type() && self.0.date() == other.0.date()
}
}
impl Eq for ActionOrder {}
impl PartialOrd for ActionOrder {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0
.date()
.partial_cmp(&other.0.date())
.map(|date_order| date_order.then(self.0.action_type().cmp(&other.0.action_type())))
}
}
impl Ord for ActionOrder {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.date()
.cmp(&other.0.date())
.then(self.0.action_type().cmp(&other.0.action_type()))
}
}
#[derive(Debug, Clone)]
pub struct Transaction {
pub description: Option<String>,
pub date: NaiveDate,
pub elements: Vec<TransactionElement>,
}
impl Transaction {
pub fn new(
description: Option<String>,
date: NaiveDate,
elements: Vec<TransactionElement>,
) -> Transaction {
Transaction {
description,
date,
elements,
}
}
pub fn new_simple(
description: Option<&str>,
date: NaiveDate,
from_account: Rc<Account>,
to_account: Rc<Account>,
amount: Commodity,
exchange_rate: Option<ExchangeRate>,
) -> Transaction {
Transaction::new(
description.map(|s| String::from(s)),
date,
vec![
TransactionElement::new(from_account, Some(amount.neg()), exchange_rate.clone()),
TransactionElement::new(to_account, None, exchange_rate),
],
)
}
pub fn get_element(&self, account: &Account) -> Option<&TransactionElement> {
self.elements.iter().find(|e| e.account.as_ref() == account)
}
}
impl fmt::Display for Transaction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Transaction")
}
}
impl Action for Transaction {
fn date(&self) -> NaiveDate {
self.date
}
fn perform(&self, program_state: &mut ProgramState) -> Result<(), AccountingError> {
if self.elements.len() < 2 {
return Err(AccountingError::InvalidTransaction(
self.clone(),
String::from("a transaction cannot have less than 2 elements"),
));
}
let mut empty_amount_element: Option<usize> = None;
for (i, element) in self.elements.iter().enumerate() {
if element.amount.is_none() {
if empty_amount_element.is_none() {
empty_amount_element = Some(i)
} else {
return Err(AccountingError::InvalidTransaction(
self.clone(),
String::from("multiple elements with no amount specified"),
));
}
}
}
let sum_currency = match empty_amount_element {
Some(empty_i) => {
let empty_element = self.elements.get(empty_i).unwrap();
empty_element.account.currency.clone()
}
None => self
.elements
.get(0)
.expect("there should be at least 2 elements in the transaction")
.account
.currency
.clone(),
};
let mut sum = Commodity::new(Decimal::zero(), sum_currency.code);
let mut modified_elements = self.elements.clone();
for (i, element) in self.elements.iter().enumerate() {
match empty_amount_element {
Some(empty_i) => {
if i != empty_i {
sum = match sum.add(&element.amount.as_ref().unwrap()) {
Ok(value) => value,
Err(error) => return Err(AccountingError::Currency(error)),
}
}
}
None => {}
}
}
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::Currency(error)),
}
}
None => {}
};
if sum.value != Decimal::zero() {
return Err(AccountingError::InvalidTransaction(
self.clone(),
String::from("sum of transaction elements does not equal zero"),
));
}
for transaction in &modified_elements {
let mut account_state = program_state
.get_account_state_mut(&transaction.account.id)
.expect(
format!(
"unable to find state for account with id: {}, name: {:?} please ensure this account was added to the program state before execution.",
transaction.account.id,
transaction.account.name
)
.as_ref(),
);
match account_state.status {
AccountStatus::Closed => Err(AccountingError::InvalidAccountStatus {
account: transaction.account.clone(),
status: account_state.status,
}),
_ => Ok(()),
}?;
let transaction_amount = match &transaction.amount {
Some(amount) => amount,
None => {
return Err(AccountingError::InvalidTransaction(
self.clone(),
String::from(
"unable to calculate all required amounts for this transaction",
),
))
}
};
account_state.amount = match account_state.amount.add(transaction_amount) {
Ok(commodity) => commodity,
Err(err) => {
return Err(AccountingError::Currency(err));
}
}
}
return Ok(());
}
fn action_type(&self) -> ActionType {
ActionType::Transaction
}
}
#[derive(Debug, Clone)]
pub struct TransactionElement {
pub account: Rc<Account>,
pub amount: Option<Commodity>,
pub exchange_rate: Option<ExchangeRate>,
}
impl TransactionElement {
pub fn new(
account: Rc<Account>,
amount: Option<Commodity>,
exchange_rate: Option<ExchangeRate>,
) -> TransactionElement {
TransactionElement {
account,
amount,
exchange_rate,
}
}
}
#[derive(Debug)]
pub struct EditAccountStatus {
account: Rc<Account>,
newstatus: AccountStatus,
date: NaiveDate,
}
impl EditAccountStatus {
pub fn new(
account: Rc<Account>,
newstatus: AccountStatus,
date: NaiveDate,
) -> EditAccountStatus {
EditAccountStatus {
account,
newstatus,
date,
}
}
}
impl fmt::Display for EditAccountStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Edit Account Status")
}
}
impl Action for EditAccountStatus {
fn date(&self) -> NaiveDate {
self.date
}
fn perform(&self, program_state: &mut ProgramState) -> Result<(), AccountingError> {
let mut account_state = program_state
.get_account_state_mut(&self.account.id)
.unwrap();
account_state.status = self.newstatus;
return Ok(());
}
fn action_type(&self) -> ActionType {
ActionType::EditAccountStatus
}
}
#[derive(Debug, Clone)]
pub struct BalanceAssertion {
account: Rc<Account>,
date: NaiveDate,
expected_balance: Commodity,
}
impl BalanceAssertion {
pub fn new(
account: Rc<Account>,
date: NaiveDate,
expected_balance: Commodity,
) -> BalanceAssertion {
BalanceAssertion {
account,
date,
expected_balance,
}
}
}
impl fmt::Display for BalanceAssertion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Assert Account Balance")
}
}
#[derive(Debug, Clone)]
pub struct FailedBalanceAssertion {
pub assertion: BalanceAssertion,
pub actual_balance: Commodity,
}
impl FailedBalanceAssertion {
pub fn new(assertion: BalanceAssertion, actual_balance: Commodity) -> FailedBalanceAssertion {
FailedBalanceAssertion {
assertion,
actual_balance,
}
}
}
impl fmt::Display for FailedBalanceAssertion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Failed Account Balance Assertion")
}
}
impl Action for BalanceAssertion {
fn date(&self) -> NaiveDate {
self.date
}
fn perform(&self, program_state: &mut ProgramState) -> Result<(), AccountingError> {
match program_state.get_account_state(&self.account.id) {
Some(state) => {
if state
.amount
.eq_approx(self.expected_balance, Commodity::default_epsilon())
{
} else {
}
}
None => {
return Err(AccountingError::MissingAccountState(
self.account.id.clone(),
));
}
}
return Ok(());
}
fn action_type(&self) -> ActionType {
ActionType::BalanceAssertion
}
}
#[cfg(test)]
mod tests {
use super::ActionType;
use std::collections::HashSet;
#[test]
fn action_type_order() {
let mut tested_types: HashSet<ActionType> = HashSet::new();
let mut action_types_unordered: Vec<ActionType> = vec![
ActionType::Transaction,
ActionType::EditAccountStatus,
ActionType::BalanceAssertion,
ActionType::EditAccountStatus,
ActionType::Transaction,
ActionType::BalanceAssertion,
];
let num_action_types = ActionType::iterator().count();
action_types_unordered.iter().for_each(|action_type| {
tested_types.insert(action_type.clone());
});
assert_eq!(num_action_types, tested_types.len());
action_types_unordered.sort();
let action_types_ordered: Vec<ActionType> = vec![
ActionType::EditAccountStatus,
ActionType::EditAccountStatus,
ActionType::BalanceAssertion,
ActionType::BalanceAssertion,
ActionType::Transaction,
ActionType::Transaction,
];
assert_eq!(action_types_ordered, action_types_unordered);
}
}