use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use super::ids::AccountId;
use super::money::Money;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AccountType {
#[default]
Checking,
Savings,
Credit,
Cash,
Investment,
LineOfCredit,
Other,
}
impl AccountType {
pub fn is_liability(&self) -> bool {
matches!(self, Self::Credit | Self::LineOfCredit)
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"checking" => Some(Self::Checking),
"savings" => Some(Self::Savings),
"credit" | "credit_card" | "creditcard" => Some(Self::Credit),
"cash" => Some(Self::Cash),
"investment" => Some(Self::Investment),
"line_of_credit" | "lineofcredit" | "loc" => Some(Self::LineOfCredit),
"other" => Some(Self::Other),
_ => None,
}
}
}
impl fmt::Display for AccountType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Checking => write!(f, "Checking"),
Self::Savings => write!(f, "Savings"),
Self::Credit => write!(f, "Credit Card"),
Self::Cash => write!(f, "Cash"),
Self::Investment => write!(f, "Investment"),
Self::LineOfCredit => write!(f, "Line of Credit"),
Self::Other => write!(f, "Other"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account {
pub id: AccountId,
pub name: String,
#[serde(rename = "type")]
pub account_type: AccountType,
pub on_budget: bool,
pub archived: bool,
pub starting_balance: Money,
#[serde(default)]
pub notes: String,
pub last_reconciled_date: Option<NaiveDate>,
pub last_reconciled_balance: Option<Money>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub sort_order: i32,
}
impl Account {
pub fn new(name: impl Into<String>, account_type: AccountType) -> Self {
let now = Utc::now();
Self {
id: AccountId::new(),
name: name.into(),
account_type,
on_budget: true,
archived: false,
starting_balance: Money::zero(),
notes: String::new(),
last_reconciled_date: None,
last_reconciled_balance: None,
created_at: now,
updated_at: now,
sort_order: 0,
}
}
pub fn with_starting_balance(
name: impl Into<String>,
account_type: AccountType,
starting_balance: Money,
) -> Self {
let mut account = Self::new(name, account_type);
account.starting_balance = starting_balance;
account
}
pub fn archive(&mut self) {
self.archived = true;
self.updated_at = Utc::now();
}
pub fn unarchive(&mut self) {
self.archived = false;
self.updated_at = Utc::now();
}
pub fn set_on_budget(&mut self, on_budget: bool) {
self.on_budget = on_budget;
self.updated_at = Utc::now();
}
pub fn reconcile(&mut self, date: NaiveDate, balance: Money) {
self.last_reconciled_date = Some(date);
self.last_reconciled_balance = Some(balance);
self.updated_at = Utc::now();
}
pub fn validate(&self) -> Result<(), AccountValidationError> {
if self.name.trim().is_empty() {
return Err(AccountValidationError::EmptyName);
}
if self.name.len() > 100 {
return Err(AccountValidationError::NameTooLong(self.name.len()));
}
Ok(())
}
}
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.account_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccountValidationError {
EmptyName,
NameTooLong(usize),
}
impl fmt::Display for AccountValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyName => write!(f, "Account name cannot be empty"),
Self::NameTooLong(len) => {
write!(f, "Account name too long ({} chars, max 100)", len)
}
}
}
}
impl std::error::Error for AccountValidationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_account() {
let account = Account::new("Checking", AccountType::Checking);
assert_eq!(account.name, "Checking");
assert_eq!(account.account_type, AccountType::Checking);
assert!(account.on_budget);
assert!(!account.archived);
assert_eq!(account.starting_balance, Money::zero());
}
#[test]
fn test_with_starting_balance() {
let account = Account::with_starting_balance(
"Savings",
AccountType::Savings,
Money::from_cents(100000),
);
assert_eq!(account.starting_balance.cents(), 100000);
}
#[test]
fn test_archive() {
let mut account = Account::new("Test", AccountType::Checking);
assert!(!account.archived);
account.archive();
assert!(account.archived);
account.unarchive();
assert!(!account.archived);
}
#[test]
fn test_validation() {
let mut account = Account::new("Valid Name", AccountType::Checking);
assert!(account.validate().is_ok());
account.name = String::new();
assert_eq!(account.validate(), Err(AccountValidationError::EmptyName));
account.name = "a".repeat(101);
assert!(matches!(
account.validate(),
Err(AccountValidationError::NameTooLong(_))
));
}
#[test]
fn test_account_type_parsing() {
assert_eq!(AccountType::parse("checking"), Some(AccountType::Checking));
assert_eq!(AccountType::parse("SAVINGS"), Some(AccountType::Savings));
assert_eq!(AccountType::parse("credit_card"), Some(AccountType::Credit));
assert_eq!(AccountType::parse("invalid"), None);
}
#[test]
fn test_is_liability() {
assert!(AccountType::Credit.is_liability());
assert!(AccountType::LineOfCredit.is_liability());
assert!(!AccountType::Checking.is_liability());
assert!(!AccountType::Savings.is_liability());
}
#[test]
fn test_serialization() {
let account = Account::new("Test", AccountType::Checking);
let json = serde_json::to_string(&account).unwrap();
let deserialized: Account = serde_json::from_str(&json).unwrap();
assert_eq!(account.id, deserialized.id);
assert_eq!(account.name, deserialized.name);
}
#[test]
fn test_display() {
let account = Account::new("My Checking", AccountType::Checking);
assert_eq!(format!("{}", account), "My Checking (Checking)");
}
}