1use chrono::{DateTime, NaiveDate, Utc};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9use super::ids::AccountId;
10use super::money::Money;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "lowercase")]
15pub enum AccountType {
16 #[default]
18 Checking,
19 Savings,
21 Credit,
23 Cash,
25 Investment,
27 LineOfCredit,
29 Other,
31}
32
33impl AccountType {
34 pub fn is_liability(&self) -> bool {
37 matches!(self, Self::Credit | Self::LineOfCredit)
38 }
39
40 pub fn parse(s: &str) -> Option<Self> {
42 match s.to_lowercase().as_str() {
43 "checking" => Some(Self::Checking),
44 "savings" => Some(Self::Savings),
45 "credit" | "credit_card" | "creditcard" => Some(Self::Credit),
46 "cash" => Some(Self::Cash),
47 "investment" => Some(Self::Investment),
48 "line_of_credit" | "lineofcredit" | "loc" => Some(Self::LineOfCredit),
49 "other" => Some(Self::Other),
50 _ => None,
51 }
52 }
53}
54
55impl fmt::Display for AccountType {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Checking => write!(f, "Checking"),
59 Self::Savings => write!(f, "Savings"),
60 Self::Credit => write!(f, "Credit Card"),
61 Self::Cash => write!(f, "Cash"),
62 Self::Investment => write!(f, "Investment"),
63 Self::LineOfCredit => write!(f, "Line of Credit"),
64 Self::Other => write!(f, "Other"),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Account {
72 pub id: AccountId,
74
75 pub name: String,
77
78 #[serde(rename = "type")]
80 pub account_type: AccountType,
81
82 pub on_budget: bool,
85
86 pub archived: bool,
88
89 pub starting_balance: Money,
91
92 #[serde(default)]
94 pub notes: String,
95
96 pub last_reconciled_date: Option<NaiveDate>,
98
99 pub last_reconciled_balance: Option<Money>,
101
102 pub created_at: DateTime<Utc>,
104
105 pub updated_at: DateTime<Utc>,
107
108 #[serde(default)]
110 pub sort_order: i32,
111}
112
113impl Account {
114 pub fn new(name: impl Into<String>, account_type: AccountType) -> Self {
116 let now = Utc::now();
117 Self {
118 id: AccountId::new(),
119 name: name.into(),
120 account_type,
121 on_budget: true,
122 archived: false,
123 starting_balance: Money::zero(),
124 notes: String::new(),
125 last_reconciled_date: None,
126 last_reconciled_balance: None,
127 created_at: now,
128 updated_at: now,
129 sort_order: 0,
130 }
131 }
132
133 pub fn with_starting_balance(
135 name: impl Into<String>,
136 account_type: AccountType,
137 starting_balance: Money,
138 ) -> Self {
139 let mut account = Self::new(name, account_type);
140 account.starting_balance = starting_balance;
141 account
142 }
143
144 pub fn archive(&mut self) {
146 self.archived = true;
147 self.updated_at = Utc::now();
148 }
149
150 pub fn unarchive(&mut self) {
152 self.archived = false;
153 self.updated_at = Utc::now();
154 }
155
156 pub fn set_on_budget(&mut self, on_budget: bool) {
158 self.on_budget = on_budget;
159 self.updated_at = Utc::now();
160 }
161
162 pub fn reconcile(&mut self, date: NaiveDate, balance: Money) {
164 self.last_reconciled_date = Some(date);
165 self.last_reconciled_balance = Some(balance);
166 self.updated_at = Utc::now();
167 }
168
169 pub fn validate(&self) -> Result<(), AccountValidationError> {
171 if self.name.trim().is_empty() {
172 return Err(AccountValidationError::EmptyName);
173 }
174
175 if self.name.len() > 100 {
176 return Err(AccountValidationError::NameTooLong(self.name.len()));
177 }
178
179 Ok(())
180 }
181}
182
183impl fmt::Display for Account {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 write!(f, "{} ({})", self.name, self.account_type)
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum AccountValidationError {
192 EmptyName,
193 NameTooLong(usize),
194}
195
196impl fmt::Display for AccountValidationError {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 match self {
199 Self::EmptyName => write!(f, "Account name cannot be empty"),
200 Self::NameTooLong(len) => {
201 write!(f, "Account name too long ({} chars, max 100)", len)
202 }
203 }
204 }
205}
206
207impl std::error::Error for AccountValidationError {}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_new_account() {
215 let account = Account::new("Checking", AccountType::Checking);
216 assert_eq!(account.name, "Checking");
217 assert_eq!(account.account_type, AccountType::Checking);
218 assert!(account.on_budget);
219 assert!(!account.archived);
220 assert_eq!(account.starting_balance, Money::zero());
221 }
222
223 #[test]
224 fn test_with_starting_balance() {
225 let account = Account::with_starting_balance(
226 "Savings",
227 AccountType::Savings,
228 Money::from_cents(100000),
229 );
230 assert_eq!(account.starting_balance.cents(), 100000);
231 }
232
233 #[test]
234 fn test_archive() {
235 let mut account = Account::new("Test", AccountType::Checking);
236 assert!(!account.archived);
237
238 account.archive();
239 assert!(account.archived);
240
241 account.unarchive();
242 assert!(!account.archived);
243 }
244
245 #[test]
246 fn test_validation() {
247 let mut account = Account::new("Valid Name", AccountType::Checking);
248 assert!(account.validate().is_ok());
249
250 account.name = String::new();
251 assert_eq!(account.validate(), Err(AccountValidationError::EmptyName));
252
253 account.name = "a".repeat(101);
254 assert!(matches!(
255 account.validate(),
256 Err(AccountValidationError::NameTooLong(_))
257 ));
258 }
259
260 #[test]
261 fn test_account_type_parsing() {
262 assert_eq!(AccountType::parse("checking"), Some(AccountType::Checking));
263 assert_eq!(AccountType::parse("SAVINGS"), Some(AccountType::Savings));
264 assert_eq!(AccountType::parse("credit_card"), Some(AccountType::Credit));
265 assert_eq!(AccountType::parse("invalid"), None);
266 }
267
268 #[test]
269 fn test_is_liability() {
270 assert!(AccountType::Credit.is_liability());
271 assert!(AccountType::LineOfCredit.is_liability());
272 assert!(!AccountType::Checking.is_liability());
273 assert!(!AccountType::Savings.is_liability());
274 }
275
276 #[test]
277 fn test_serialization() {
278 let account = Account::new("Test", AccountType::Checking);
279 let json = serde_json::to_string(&account).unwrap();
280 let deserialized: Account = serde_json::from_str(&json).unwrap();
281 assert_eq!(account.id, deserialized.id);
282 assert_eq!(account.name, deserialized.name);
283 }
284
285 #[test]
286 fn test_display() {
287 let account = Account::new("My Checking", AccountType::Checking);
288 assert_eq!(format!("{}", account), "My Checking (Checking)");
289 }
290}