Skip to main content

doublecount/
actions.rs

1use super::{AccountID, AccountStatus, AccountingError, ProgramState};
2use chrono::NaiveDate;
3use commodity::exchange_rate::ExchangeRate;
4use commodity::Commodity;
5use rust_decimal::{prelude::Zero, Decimal};
6use std::fmt;
7use std::rc::Rc;
8use std::{marker::PhantomData, slice};
9
10#[cfg(feature = "serde-support")]
11use serde::{Deserialize, Serialize};
12
13/// A representation of what type of [Action](Action) is being performed.
14#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Hash, Clone)]
15pub enum ActionType {
16    /// An [Action](Action) to edit the status of an [Account](crate::Account).
17    /// Represented by the [EditAccountStatus](EditAccountStatus) struct.
18    ///
19    /// This action has the highest priority when being sorted, because
20    /// other actions on the same day may depend on this already having
21    /// been executed.
22    EditAccountStatus,
23    /// An [Action](Action) to assert the current balance of an account while
24    /// a [Program](super::Program) is being executed. Represented by a
25    /// [BalanceAssertion](BalanceAssertion) struct.
26    BalanceAssertion,
27    /// A [Action](Action) to perform a transaction between [Account](crate::Account)s.
28    /// Represented by the [Transaction](Transaction) struct.
29    Transaction,
30}
31
32impl ActionTypeFor<ActionType> for ActionTypeValue {
33    fn action_type(&self) -> ActionType {
34        match self {
35            ActionTypeValue::EditAccountStatus(_) => ActionType::EditAccountStatus,
36            ActionTypeValue::BalanceAssertion(_) => ActionType::BalanceAssertion,
37            ActionTypeValue::Transaction(_) => ActionType::Transaction,
38        }
39    }
40}
41
42impl ActionType {
43    /// Return an iterator over all available [ActionType](ActionType) variants.
44    pub fn iterator() -> slice::Iter<'static, ActionType> {
45        static ACTION_TYPES: [ActionType; 3] = [
46            ActionType::EditAccountStatus,
47            ActionType::BalanceAssertion,
48            ActionType::Transaction,
49        ];
50        ACTION_TYPES.iter()
51    }
52}
53
54/// A trait which represents an enum/sized data structure which is
55/// capable of storing every possible concrete implementation of
56/// [Action](Action) for your [Program](crate::Program).
57///
58/// If you have some custom actions, you need to implement this trait
59/// yourself and use it to store your actions that you provide to
60/// [Program](crate::Program).
61pub trait ActionTypeValueEnum<AT> {
62    fn as_action(&self) -> &dyn Action<AT, Self>;
63}
64
65/// An enum to store every possible concrete implementation of
66/// [Action](Action) in a `Sized` element.
67#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
68#[cfg_attr(feature = "serde-support", serde(tag = "type"))]
69#[derive(Debug, Clone, PartialEq)]
70pub enum ActionTypeValue {
71    EditAccountStatus(EditAccountStatus),
72    BalanceAssertion(BalanceAssertion),
73    Transaction(Transaction),
74}
75
76impl<AT> ActionTypeValueEnum<AT> for ActionTypeValue {
77    fn as_action(&self) -> &dyn Action<AT, ActionTypeValue> {
78        match self {
79            ActionTypeValue::EditAccountStatus(action) => action,
80            ActionTypeValue::BalanceAssertion(action) => action,
81            ActionTypeValue::Transaction(action) => action,
82        }
83    }
84}
85
86impl From<EditAccountStatus> for ActionTypeValue {
87    fn from(action: EditAccountStatus) -> Self {
88        ActionTypeValue::EditAccountStatus(action)
89    }
90}
91
92impl From<BalanceAssertion> for ActionTypeValue {
93    fn from(action: BalanceAssertion) -> Self {
94        ActionTypeValue::BalanceAssertion(action)
95    }
96}
97
98impl From<Transaction> for ActionTypeValue {
99    fn from(action: Transaction) -> Self {
100        ActionTypeValue::Transaction(action)
101    }
102}
103
104/// Obtain the concrete action type for an action.
105pub trait ActionTypeFor<AT> {
106    /// What type of action is being performed.
107    fn action_type(&self) -> AT;
108}
109
110/// Represents an action which can modify [ProgramState](ProgramState).
111pub trait Action<AT, ATV>: fmt::Display + fmt::Debug {
112    /// The date/time (in the account history) that the action was performed.
113    fn date(&self) -> NaiveDate;
114
115    /// Perform the action to mutate the [ProgramState](ProgramState).
116    fn perform(&self, program_state: &mut ProgramState<AT, ATV>) -> Result<(), AccountingError>;
117}
118
119/// A way to sort [Action](Action)s by their date, and then by the
120/// priority of their [ActionType](ActionType).
121///
122/// # Example
123/// ```
124/// use doublecount::{ActionTypeValue, ActionOrder};
125/// use std::rc::Rc;
126///
127/// let mut actions: Vec<Rc<ActionTypeValue>> = Vec::new();
128///
129/// // let's pretend we created and added
130/// // some actions to the actions vector
131///
132/// // sort the actions using this order
133/// actions.sort_by_key(|a| ActionOrder::new(a.clone()));
134/// ```
135pub struct ActionOrder<AT, ATV> {
136    action_value: Rc<ATV>,
137    action_type: PhantomData<AT>,
138}
139
140impl<AT, ATV> ActionOrder<AT, ATV> {
141    pub fn new(action_value: Rc<ATV>) -> Self {
142        Self {
143            action_value,
144            action_type: PhantomData::default(),
145        }
146    }
147}
148
149impl<AT, ATV> PartialEq for ActionOrder<AT, ATV>
150where
151    AT: PartialEq,
152    ATV: ActionTypeValueEnum<AT> + ActionTypeFor<AT>,
153{
154    fn eq(&self, other: &ActionOrder<AT, ATV>) -> bool {
155        let self_action = self.action_value.as_action();
156        let other_action = other.action_value.as_action();
157        self.action_value.action_type() == other.action_value.action_type()
158            && self_action.date() == other_action.date()
159    }
160}
161
162impl<AT, ATV> Eq for ActionOrder<AT, ATV>
163where
164    ATV: ActionTypeValueEnum<AT> + ActionTypeFor<AT>,
165    AT: PartialEq,
166{
167}
168
169impl<AT, ATV> PartialOrd for ActionOrder<AT, ATV>
170where
171    AT: Ord,
172    ATV: ActionTypeValueEnum<AT> + ActionTypeFor<AT>,
173{
174    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
175        let self_action = self.action_value.as_action();
176        let other_action = other.action_value.as_action();
177        self_action
178            .date()
179            .partial_cmp(&other_action.date())
180            .map(|date_order| {
181                date_order.then(
182                    self.action_value
183                        .action_type()
184                        .cmp(&other.action_value.action_type()),
185                )
186            })
187    }
188}
189
190impl<AT, ATV> Ord for ActionOrder<AT, ATV>
191where
192    AT: Ord,
193    ATV: ActionTypeValueEnum<AT> + ActionTypeFor<AT>,
194{
195    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
196        let self_action = self.action_value.as_action();
197        let other_action = other.action_value.as_action();
198        self_action.date().cmp(&other_action.date()).then(
199            self.action_value
200                .action_type()
201                .cmp(&other.action_value.action_type()),
202        )
203    }
204}
205
206/// A movement of [Commodity](Commodity) between two or more accounts
207/// on a given `date`. Implements [Action](Action) so it can be
208/// applied to change [AccountState](super::AccountState)s.
209///
210/// The sum of the [Commodity](Commodity) `amount`s contained within a
211/// transaction's [TransactionElement](TransactionElement)s needs to
212/// be equal to zero, or one of the elements needs to have a `None`
213/// value `amount`.
214#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
215#[derive(Debug, Clone, PartialEq)]
216pub struct Transaction {
217    /// Description of this transaction.
218    pub description: Option<String>,
219    /// The date that the transaction occurred.
220    pub date: NaiveDate,
221    /// Elements which compose this transaction.
222    ///
223    /// See [Transaction](Transaction) for more information about the
224    /// constraints which apply to this field.
225    pub elements: Vec<TransactionElement>,
226}
227
228impl Transaction {
229    /// Create a new [Transaction](Transaction).
230    pub fn new<S: Into<String>>(
231        description: Option<S>,
232        date: NaiveDate,
233        elements: Vec<TransactionElement>,
234    ) -> Transaction {
235        Transaction {
236            description: description.map(|s| s.into()),
237            date,
238            elements,
239        }
240    }
241
242    /// Create a new simple [Transaction](Transaction), containing
243    /// only two elements, transfering an `amount` from `from_account`
244    /// to `to_account` on the given `date`, with the given
245    /// `exchange_rate` (required if the currencies of the accounts
246    /// are different).
247    ///
248    /// # Example
249    /// ```
250    /// # use doublecount::Transaction;
251    /// # use std::rc::Rc;
252    /// use doublecount::Account;
253    /// use commodity::{CommodityType, Commodity};
254    /// use chrono::Local;
255    /// use std::str::FromStr;
256    ///
257    /// let aud = Rc::from(CommodityType::from_str("AUD", "Australian Dollar").unwrap());
258    ///
259    /// let account1 = Rc::from(Account::new_with_id(Some("Account 1"), aud.id, None));
260    /// let account2 = Rc::from(Account::new_with_id(Some("Account 2"), aud.id, None));
261    ///
262    /// let transaction = Transaction::new_simple(
263    ///    Some("balancing"),
264    ///    Local::today().naive_local(),
265    ///    account1.id,
266    ///    account2.id,
267    ///    Commodity::from_str("100.0 AUD").unwrap(),
268    ///    None,
269    /// );
270    ///
271    /// assert_eq!(2, transaction.elements.len());
272    /// let element0 = transaction.elements.get(0).unwrap();
273    /// let element1 = transaction.elements.get(1).unwrap();
274    /// assert_eq!(Some(Commodity::from_str("-100.0 AUD").unwrap()), element0.amount);
275    /// assert_eq!(account1.id, element0.account_id);
276    /// assert_eq!(account2.id, element1.account_id);
277    /// assert_eq!(None, element1.amount);
278    /// ```
279    pub fn new_simple<S: Into<String>>(
280        description: Option<S>,
281        date: NaiveDate,
282        from_account: AccountID,
283        to_account: AccountID,
284        amount: Commodity,
285        exchange_rate: Option<ExchangeRate>,
286    ) -> Transaction {
287        Transaction::new(
288            description,
289            date,
290            vec![
291                TransactionElement::new(from_account, Some(amount.neg()), exchange_rate.clone()),
292                TransactionElement::new(to_account, None, exchange_rate),
293            ],
294        )
295    }
296
297    /// Get the [TransactionElement](TransactionElement) associated
298    /// with the given [Account](crate::Account)'s id.
299    pub fn get_element(&self, account_id: &AccountID) -> Option<&TransactionElement> {
300        self.elements.iter().find(|e| &e.account_id == account_id)
301    }
302}
303
304impl ActionTypeFor<ActionType> for Transaction {
305    fn action_type(&self) -> ActionType {
306        todo!()
307    }
308}
309
310impl fmt::Display for Transaction {
311    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
312        write!(f, "Transaction")
313    }
314}
315
316impl<AT, ATV> Action<AT, ATV> for Transaction
317where
318    ATV: ActionTypeValueEnum<AT>,
319{
320    fn date(&self) -> NaiveDate {
321        self.date
322    }
323
324    fn perform(&self, program_state: &mut ProgramState<AT, ATV>) -> Result<(), AccountingError> {
325        // check that the transaction has at least 2 elements
326        if self.elements.len() < 2 {
327            return Err(AccountingError::InvalidTransaction(
328                self.clone(),
329                String::from("a transaction cannot have less than 2 elements"),
330            ));
331        }
332
333        //TODO: add check to ensure that transaction doesn't have duplicate account references?
334
335        // first process the elements to automatically calculate amounts
336
337        let mut empty_amount_element: Option<usize> = None;
338        for (i, element) in self.elements.iter().enumerate() {
339            if element.amount.is_none() {
340                if empty_amount_element.is_none() {
341                    empty_amount_element = Some(i)
342                } else {
343                    return Err(AccountingError::InvalidTransaction(
344                        self.clone(),
345                        String::from("multiple elements with no amount specified"),
346                    ));
347                }
348            }
349        }
350
351        let sum_commodity_type_id = match empty_amount_element {
352            Some(empty_i) => {
353                let empty_element = self.elements.get(empty_i).unwrap();
354
355                match program_state.get_account(&empty_element.account_id) {
356                    Some(account) => account.commodity_type_id,
357                    None => {
358                        return Err(AccountingError::MissingAccountState(
359                            empty_element.account_id,
360                        ))
361                    }
362                }
363            }
364            None => {
365                let account_id = self
366                    .elements
367                    .get(0)
368                    .expect("there should be at least 2 elements in the transaction")
369                    .account_id;
370
371                match program_state.get_account(&account_id) {
372                    Some(account) => account.commodity_type_id,
373                    None => return Err(AccountingError::MissingAccountState(account_id)),
374                }
375            }
376        };
377
378        let mut sum = Commodity::new(Decimal::zero(), sum_commodity_type_id);
379
380        let mut modified_elements = self.elements.clone();
381
382        // Calculate the sum of elements (not including the empty element if there is one)
383        for (i, element) in self.elements.iter().enumerate() {
384            if let Some(empty_i) = empty_amount_element {
385                if i != empty_i {
386                    //TODO: perform commodity type conversion here if required
387                    sum = match sum.add(&element.amount.as_ref().unwrap()) {
388                        Ok(value) => value,
389                        Err(error) => return Err(AccountingError::Commodity(error)),
390                    }
391                }
392            }
393        }
394
395        // Calculate the value to use for the empty element (negate the sum of the other elements)
396        if let Some(empty_i) = empty_amount_element {
397            let modified_emtpy_element: &mut TransactionElement =
398                modified_elements.get_mut(empty_i).unwrap();
399            let negated_sum = sum.neg();
400            modified_emtpy_element.amount = Some(negated_sum);
401
402            sum = match sum.add(&negated_sum) {
403                Ok(value) => value,
404                Err(error) => return Err(AccountingError::Commodity(error)),
405            }
406        }
407
408        if sum.value != Decimal::zero() {
409            return Err(AccountingError::InvalidTransaction(
410                self.clone(),
411                String::from("sum of transaction elements does not equal zero"),
412            ));
413        }
414
415        for transaction in &modified_elements {
416            let mut account_state = program_state
417                .get_account_state_mut(&transaction.account_id)
418                .unwrap_or_else(||
419                    panic!(
420                        "unable to find state for account with id: {} please ensure this account was added to the program state before execution.",
421                        transaction.account_id
422                    )
423                );
424
425            match account_state.status {
426                AccountStatus::Closed => Err(AccountingError::InvalidAccountStatus {
427                    account_id: transaction.account_id,
428                    status: account_state.status,
429                }),
430                _ => Ok(()),
431            }?;
432
433            // TODO: perform the commodity type conversion using the exchange rate (if present)
434
435            let transaction_amount = match &transaction.amount {
436                Some(amount) => amount,
437                None => {
438                    return Err(AccountingError::InvalidTransaction(
439                        self.clone(),
440                        String::from(
441                            "unable to calculate all required amounts for this transaction",
442                        ),
443                    ))
444                }
445            };
446
447            account_state.amount = match account_state.amount.add(transaction_amount) {
448                Ok(commodity) => commodity,
449                Err(err) => {
450                    return Err(AccountingError::Commodity(err));
451                }
452            }
453        }
454
455        Ok(())
456    }
457}
458
459/// An element of a [Transaction](Transaction).
460#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
461#[derive(Debug, Clone, PartialEq)]
462pub struct TransactionElement {
463    /// The account to perform the transaction to
464    pub account_id: AccountID,
465
466    /// The amount of [Commodity](Commodity) to add to the account.
467    ///
468    /// This may be `None`, if it is the only element within a
469    /// [Transaction](Transaction), which is None. If it is `None`,
470    /// it's amount will be automatically calculated from the amounts
471    /// in the other elements present in the transaction.
472    pub amount: Option<Commodity>,
473
474    /// The exchange rate to use for converting the amount in this element
475    /// to a different [CommodityType](commodity::CommodityType).
476    pub exchange_rate: Option<ExchangeRate>,
477}
478
479impl TransactionElement {
480    /// Create a new [TransactionElement](TransactionElement).
481    pub fn new(
482        account_id: AccountID,
483        amount: Option<Commodity>,
484        exchange_rate: Option<ExchangeRate>,
485    ) -> TransactionElement {
486        TransactionElement {
487            account_id,
488            amount,
489            exchange_rate,
490        }
491    }
492}
493
494/// A type of [Action](Action) to edit the
495/// [AccountStatus](AccountStatus) of a given [Account](crate::Account)'s
496/// [AccountState](super::AccountState).
497#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
498#[derive(Debug, Clone, PartialEq)]
499pub struct EditAccountStatus {
500    account_id: AccountID,
501    newstatus: AccountStatus,
502    date: NaiveDate,
503}
504
505impl EditAccountStatus {
506    /// Create a new [EditAccountStatus](EditAccountStatus).
507    pub fn new(
508        account_id: AccountID,
509        newstatus: AccountStatus,
510        date: NaiveDate,
511    ) -> EditAccountStatus {
512        EditAccountStatus {
513            account_id,
514            newstatus,
515            date,
516        }
517    }
518}
519
520impl fmt::Display for EditAccountStatus {
521    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
522        write!(f, "Edit Account Status")
523    }
524}
525
526impl<AT, ATV> Action<AT, ATV> for EditAccountStatus
527where
528    ATV: ActionTypeValueEnum<AT>,
529{
530    fn date(&self) -> NaiveDate {
531        self.date
532    }
533
534    fn perform(&self, program_state: &mut ProgramState<AT, ATV>) -> Result<(), AccountingError> {
535        let mut account_state = program_state
536            .get_account_state_mut(&self.account_id)
537            .unwrap();
538        account_state.status = self.newstatus;
539        Ok(())
540    }
541}
542
543impl ActionTypeFor<ActionType> for EditAccountStatus {
544    fn action_type(&self) -> ActionType {
545        ActionType::EditAccountStatus
546    }
547}
548
549/// A type of [Action](Action) to check and assert the balance of a
550/// given [Account](crate::Account) in its [AccountStatus](AccountStatus) at
551/// the beginning of the given date.
552///
553/// When running its [perform()](Action::perform()) method, if this
554/// assertion fails, a [FailedBalanceAssertion](FailedBalanceAssertion)
555/// will be recorded in the [ProgramState](ProgramState).
556#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
557#[derive(Debug, Clone, PartialEq)]
558pub struct BalanceAssertion {
559    account_id: AccountID,
560    date: NaiveDate,
561    expected_balance: Commodity,
562}
563
564impl BalanceAssertion {
565    /// Create a new [BalanceAssertion](BalanceAssertion). The balance
566    /// will be considered at the beginning of the provided `date`.
567    pub fn new(
568        account_id: AccountID,
569        date: NaiveDate,
570        expected_balance: Commodity,
571    ) -> BalanceAssertion {
572        BalanceAssertion {
573            account_id,
574            date,
575            expected_balance,
576        }
577    }
578}
579
580impl fmt::Display for BalanceAssertion {
581    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
582        write!(f, "Assert Account Balance")
583    }
584}
585
586/// Records the failure of a [BalanceAssertion](BalanceAssertion) when
587/// it is evaluated using its implementation of the
588/// [Action::perform()](Action::perform()) method.
589#[derive(Debug, Clone)]
590pub struct FailedBalanceAssertion {
591    pub assertion: BalanceAssertion,
592    pub actual_balance: Commodity,
593}
594
595impl FailedBalanceAssertion {
596    /// Create a new [FailedBalanceAssertion](FailedBalanceAssertion).
597    pub fn new(assertion: BalanceAssertion, actual_balance: Commodity) -> FailedBalanceAssertion {
598        FailedBalanceAssertion {
599            assertion,
600            actual_balance,
601        }
602    }
603}
604
605impl fmt::Display for FailedBalanceAssertion {
606    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
607        write!(f, "Failed Account Balance Assertion")
608    }
609}
610
611// When running this action's `perform()` method implementation, if
612// this assertion fails, a [FailedBalanceAssertion](FailedBalanceAssertion)
613// will be recorded in the [ProgramState](ProgramState).
614impl<AT, ATV> Action<AT, ATV> for BalanceAssertion
615where
616    ATV: ActionTypeValueEnum<AT>,
617{
618    fn date(&self) -> NaiveDate {
619        self.date
620    }
621
622    fn perform(&self, program_state: &mut ProgramState<AT, ATV>) -> Result<(), AccountingError> {
623        let failed_assertion = match program_state.get_account_state(&self.account_id) {
624            Some(state) => {
625                if !state
626                    .amount
627                    .eq_approx(self.expected_balance, Commodity::default_epsilon())
628                {
629                    Some(FailedBalanceAssertion::new(self.clone(), state.amount))
630                } else {
631                    None
632                }
633            }
634            None => {
635                return Err(AccountingError::MissingAccountState(self.account_id));
636            }
637        };
638
639        if let Some(failed_assertion) = failed_assertion {
640            program_state.record_failed_balance_assertion(failed_assertion)
641        }
642
643        Ok(())
644    }
645}
646
647impl ActionTypeFor<ActionType> for BalanceAssertion {
648    fn action_type(&self) -> ActionType {
649        ActionType::BalanceAssertion
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::ActionType;
656    use crate::{
657        Account, AccountStatus, AccountingError, ActionTypeValue, BalanceAssertion, Program,
658        ProgramState, Transaction,
659    };
660    use chrono::NaiveDate;
661    use commodity::{Commodity, CommodityType};
662    use rust_decimal::Decimal;
663    use std::{collections::HashSet, rc::Rc};
664
665    #[test]
666    fn action_type_order() {
667        let mut tested_types: HashSet<ActionType> = HashSet::new();
668
669        let mut action_types_unordered: Vec<ActionType> = vec![
670            ActionType::Transaction,
671            ActionType::EditAccountStatus,
672            ActionType::BalanceAssertion,
673            ActionType::EditAccountStatus,
674            ActionType::Transaction,
675            ActionType::BalanceAssertion,
676        ];
677
678        let num_action_types = ActionType::iterator().count();
679
680        action_types_unordered.iter().for_each(|action_type| {
681            tested_types.insert(action_type.clone());
682        });
683
684        assert_eq!(num_action_types, tested_types.len());
685
686        action_types_unordered.sort();
687
688        let action_types_ordered: Vec<ActionType> = vec![
689            ActionType::EditAccountStatus,
690            ActionType::EditAccountStatus,
691            ActionType::BalanceAssertion,
692            ActionType::BalanceAssertion,
693            ActionType::Transaction,
694            ActionType::Transaction,
695        ];
696
697        assert_eq!(action_types_ordered, action_types_unordered);
698    }
699
700    #[test]
701    fn balance_assertion() {
702        let aud = Rc::from(CommodityType::from_str("AUD", "Australian Dollar").unwrap());
703        let account1 = Rc::from(Account::new_with_id(Some("Account 1"), aud.id, None));
704        let account2 = Rc::from(Account::new_with_id(Some("Account 2"), aud.id, None));
705
706        let date_1 = NaiveDate::from_ymd(2020, 01, 01);
707        let date_2 = NaiveDate::from_ymd(2020, 01, 02);
708        let actions: Vec<Rc<ActionTypeValue>> = vec![
709            Rc::new(
710                Transaction::new_simple::<String>(
711                    None,
712                    date_1.clone(),
713                    account1.id,
714                    account2.id,
715                    Commodity::new(Decimal::new(100, 2), &*aud),
716                    None,
717                )
718                .into(),
719            ),
720            // This assertion is expected to fail because it occurs at the start
721            // of the day (before the transaction).
722            Rc::new(
723                BalanceAssertion::new(
724                    account2.id,
725                    date_1.clone(),
726                    Commodity::new(Decimal::new(100, 2), &*aud),
727                )
728                .into(),
729            ),
730            // This assertion is expected to pass because it occurs at the end
731            // of the day (after the transaction).
732            Rc::new(
733                BalanceAssertion::new(
734                    account2.id,
735                    date_2.clone(),
736                    Commodity::new(Decimal::new(100, 2), &*aud),
737                )
738                .into(),
739            ),
740        ];
741
742        let program = Program::new(actions);
743
744        let accounts = vec![account1, account2];
745        let mut program_state = ProgramState::new(&accounts, AccountStatus::Open);
746        match program_state.execute_program(&program) {
747            Err(AccountingError::BalanceAssertionFailed(failure)) => {
748                assert_eq!(
749                    Commodity::new(Decimal::new(0, 2), &*aud),
750                    failure.actual_balance
751                );
752                assert_eq!(date_1, failure.assertion.date);
753            }
754            _ => panic!("Expected an AccountingError:BalanceAssertionFailed"),
755        }
756
757        assert_eq!(1, program_state.failed_balance_assertions.len());
758    }
759}
760
761#[cfg(feature = "serde-support")]
762#[cfg(test)]
763mod serde_tests {
764    use super::{BalanceAssertion, EditAccountStatus, Transaction};
765    use crate::{AccountID, AccountStatus};
766    use chrono::NaiveDate;
767    use commodity::Commodity;
768    use std::str::FromStr;
769
770    #[test]
771    fn edit_account_status_serde() {
772        use serde_json;
773
774        let json = r#"{
775    "account_id": "TestAccount",
776    "newstatus": "Open",
777    "date": "2020-05-10"
778}"#;
779        let action: EditAccountStatus = serde_json::from_str(json).unwrap();
780
781        let reference_action = EditAccountStatus::new(
782            AccountID::from("TestAccount").unwrap(),
783            AccountStatus::Open,
784            NaiveDate::from_ymd(2020, 05, 10),
785        );
786
787        assert_eq!(action, reference_action);
788
789        insta::assert_json_snapshot!(action);
790    }
791
792    #[test]
793    fn balance_assertion_serde() {
794        use serde_json;
795
796        let json = r#"{
797    "account_id": "TestAccount",
798    "date": "2020-05-10",
799    "expected_balance": {
800        "value": "1.0",
801        "type_id": "AUD"
802    }
803}"#;
804        let action: BalanceAssertion = serde_json::from_str(json).unwrap();
805
806        let reference_action = BalanceAssertion::new(
807            AccountID::from("TestAccount").unwrap(),
808            NaiveDate::from_ymd(2020, 05, 10),
809            Commodity::from_str("1.0 AUD").unwrap(),
810        );
811
812        assert_eq!(action, reference_action);
813
814        insta::assert_json_snapshot!(action);
815    }
816
817    #[cfg(feature = "serde-support")]
818    #[test]
819    fn transaction_serde() {
820        use serde_json;
821
822        let json = r#"{
823    "description": "TestTransaction",
824    "date": "2020-05-10",
825    "elements": [
826        {
827            "account_id": "TestAccount1",
828            "amount": {
829                "value": "-1.0",
830                "type_id": "AUD"
831            }
832        },
833        {
834            "account_id": "TestAccount2"
835        }
836    ]  
837}"#;
838        let action: Transaction = serde_json::from_str(json).unwrap();
839
840        let reference_action = Transaction::new_simple(
841            Some("TestTransaction"),
842            NaiveDate::from_ymd(2020, 05, 10),
843            AccountID::from("TestAccount1").unwrap(),
844            AccountID::from("TestAccount2").unwrap(),
845            Commodity::from_str("1.0 AUD").unwrap(),
846            None,
847        );
848
849        assert_eq!(action, reference_action);
850
851        insta::assert_json_snapshot!(action);
852    }
853}