doublecount/
account.rs

1use arrayvec::ArrayString;
2use commodity::{Commodity, CommodityTypeID};
3use nanoid::nanoid;
4use rust_decimal::Decimal;
5use std::rc::Rc;
6
7#[cfg(feature = "serde-support")]
8use serde::{Deserialize, Serialize};
9
10/// The size in characters/bytes of the [Account](Account) id.
11const ACCOUNT_ID_LENGTH: usize = 20;
12
13/// The status of an [Account](Account) stored within an [AccountState](AccountState).
14#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
15#[derive(Copy, Clone, Debug, PartialEq)]
16pub enum AccountStatus {
17    /// The account is open
18    Open,
19    /// The account is closed
20    Closed,
21}
22/// The type to use for the id of [Account](Account)s.
23pub type AccountID = ArrayString<[u8; ACCOUNT_ID_LENGTH]>;
24
25/// A way to categorize [Account](Account)s.
26pub type AccountCategory = String;
27
28/// Details for an account, which holds a [Commodity](Commodity)
29/// with a type of [CommodityType](commodity::CommodityType).
30#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
31#[derive(Debug, Clone)]
32pub struct Account {
33    /// A unique identifier for this `Account`, currently generated using [nanoid](nanoid).
34    pub id: AccountID,
35
36    /// The name of this `Account`
37    pub name: Option<String>,
38
39    /// The id of the type of commodity to be stored in this account
40    pub commodity_type_id: CommodityTypeID,
41
42    /// The category that this account part of
43    pub category: Option<AccountCategory>,
44}
45
46impl Account {
47    /// Create a new account with an automatically generated id (using
48    /// [nanoid](nanoid)) and add it to this program state (and create
49    /// its associated [AccountState](AccountState)).
50    pub fn new_with_id<S: Into<String>>(
51        name: Option<S>,
52        commodity_type_id: CommodityTypeID,
53        category: Option<AccountCategory>,
54    ) -> Account {
55        let id_string: String = nanoid!(ACCOUNT_ID_LENGTH);
56        Self::new(
57            ArrayString::from(id_string.as_ref()).unwrap_or_else(|_| {
58                panic!(
59                    "generated id string {0} should fit within ACCOUNT_ID_LENGTH: {1}",
60                    id_string, ACCOUNT_ID_LENGTH
61                )
62            }),
63            name,
64            commodity_type_id,
65            category,
66        )
67    }
68
69    /// Create a new account and add it to this program state (and create its associated
70    /// [AccountState](AccountState)).
71    pub fn new<S: Into<String>>(
72        id: AccountID,
73        name: Option<S>,
74        commodity_type_id: CommodityTypeID,
75        category: Option<AccountCategory>,
76    ) -> Account {
77        Account {
78            id,
79            name: name.map(|s| s.into()),
80            commodity_type_id,
81            category,
82        }
83    }
84}
85
86impl PartialEq for Account {
87    fn eq(&self, other: &Account) -> bool {
88        self.id == other.id
89    }
90}
91
92/// Mutable state associated with an [Account](Account).
93#[derive(Debug, Clone, PartialEq)]
94pub struct AccountState {
95    /// The [Account](Account) associated with this state
96    pub account: Rc<Account>,
97
98    /// The amount of the commodity currently stored in this account
99    pub amount: Commodity,
100
101    /// The status of this account (open/closed/etc...)
102    pub status: AccountStatus,
103}
104
105impl AccountState {
106    /// Create a new [AccountState](AccountState).
107    pub fn new(account: Rc<Account>, amount: Commodity, status: AccountStatus) -> AccountState {
108        AccountState {
109            account,
110            amount,
111            status,
112        }
113    }
114
115    /// Open this account, set the `status` to [Open](AccountStatus::Open)
116    pub fn open(&mut self) {
117        self.status = AccountStatus::Open;
118    }
119
120    // Close this account, set the `status` to [Closed](AccountStatus::Closed)
121    pub fn close(&mut self) {
122        self.status = AccountStatus::Closed;
123    }
124
125    pub fn eq_approx(&self, other: &AccountState, epsilon: Decimal) -> bool {
126        self.account == other.account
127            && self.status == other.status
128            && self.amount.eq_approx(other.amount, epsilon)
129    }
130}
131
132#[cfg(feature = "serde-support")]
133#[cfg(test)]
134mod serde_tests {
135    use super::Account;
136    use super::AccountID;
137    use commodity::CommodityTypeID;
138    use std::str::FromStr;
139
140    #[test]
141    fn account_serde() {
142        use serde_json;
143
144        let json = r#"{
145  "id": "ABCDEFGHIJKLMNOPQRST",
146  "name": "Test Account",
147  "commodity_type_id": "USD",
148  "category": "Expense"
149}"#;
150
151        let account: Account = serde_json::from_str(json).unwrap();
152
153        let reference_account = Account::new(
154            AccountID::from("ABCDEFGHIJKLMNOPQRST").unwrap(),
155            Some("TestAccount"),
156            CommodityTypeID::from_str("AUD").unwrap(),
157            Some("Expense".to_string()),
158        );
159
160        assert_eq!(reference_account, account);
161        insta::assert_json_snapshot!(account);
162    }
163}