envelope_cli/cli/
account.rs

1//! Account CLI commands
2//!
3//! Implements CLI commands for account management.
4
5use clap::Subcommand;
6
7use crate::display::account::{format_account_details, format_account_list};
8use crate::error::EnvelopeResult;
9use crate::models::{AccountType, Money};
10use crate::services::AccountService;
11use crate::storage::Storage;
12
13/// Account subcommands
14#[derive(Subcommand)]
15pub enum AccountCommands {
16    /// Create a new account
17    Create {
18        /// Account name
19        name: String,
20        /// Account type (checking, savings, credit, cash, investment)
21        #[arg(short = 't', long, default_value = "checking")]
22        account_type: String,
23        /// Starting balance (e.g., "1000.00" or "1000")
24        #[arg(short, long, default_value = "0")]
25        balance: String,
26        /// Mark as off-budget
27        #[arg(long)]
28        off_budget: bool,
29    },
30    /// List all accounts
31    List {
32        /// Show archived accounts
33        #[arg(short, long)]
34        all: bool,
35        /// Use pretty table with borders
36        #[arg(short, long)]
37        pretty: bool,
38    },
39    /// Show account details
40    Show {
41        /// Account name or ID
42        account: String,
43    },
44    /// Edit an account
45    Edit {
46        /// Account name or ID
47        account: String,
48        /// New name
49        #[arg(short, long)]
50        name: Option<String>,
51    },
52    /// Archive an account
53    Archive {
54        /// Account name or ID
55        account: String,
56    },
57    /// Unarchive an account
58    Unarchive {
59        /// Account name or ID
60        account: String,
61    },
62}
63
64/// Handle an account command
65pub fn handle_account_command(storage: &Storage, cmd: AccountCommands) -> EnvelopeResult<()> {
66    let service = AccountService::new(storage);
67
68    match cmd {
69        AccountCommands::Create {
70            name,
71            account_type,
72            balance,
73            off_budget,
74        } => {
75            let account_type = AccountType::parse(&account_type).ok_or_else(|| {
76                crate::error::EnvelopeError::Validation(format!(
77                    "Invalid account type: '{}'. Valid types: checking, savings, credit, cash, investment, line_of_credit, other",
78                    account_type
79                ))
80            })?;
81
82            let mut starting_balance = Money::parse(&balance).map_err(|e| {
83                crate::error::EnvelopeError::Validation(format!(
84                    "Invalid balance format: '{}'. Use format like '1000.00' or '1000'. Error: {}",
85                    balance, e
86                ))
87            })?;
88
89            // For liability accounts (credit cards, lines of credit), balances represent
90            // debt owed and should be stored as negative values. Users naturally enter
91            // positive numbers when specifying debt, so we negate them.
92            if account_type.is_liability() && starting_balance.cents() > 0 {
93                starting_balance = Money::from_cents(-starting_balance.cents());
94            }
95
96            let account = service.create(&name, account_type, starting_balance, !off_budget)?;
97
98            println!("Created account: {}", account.name);
99            println!("  Type: {}", account.account_type);
100            println!("  Starting Balance: {}", account.starting_balance);
101            println!(
102                "  On Budget: {}",
103                if account.on_budget { "Yes" } else { "No" }
104            );
105            println!("  ID: {}", account.id);
106        }
107
108        AccountCommands::List { all, pretty } => {
109            let summaries = service.list_with_balances(all)?;
110            print!("{}", format_account_list(&summaries, pretty));
111        }
112
113        AccountCommands::Show { account } => {
114            let found = service
115                .find(&account)?
116                .ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
117
118            let summary = service.get_summary(&found)?;
119            print!("{}", format_account_details(&summary));
120        }
121
122        AccountCommands::Edit { account, name } => {
123            let found = service
124                .find(&account)?
125                .ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
126
127            if name.is_none() {
128                println!("No changes specified. Use --name to change the account name.");
129                return Ok(());
130            }
131
132            let updated = service.update(found.id, name.as_deref())?;
133            println!("Updated account: {}", updated.name);
134        }
135
136        AccountCommands::Archive { account } => {
137            let found = service
138                .find(&account)?
139                .ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
140
141            let archived = service.archive(found.id)?;
142            println!("Archived account: {}", archived.name);
143        }
144
145        AccountCommands::Unarchive { account } => {
146            let found = service
147                .find(&account)?
148                .ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
149
150            let unarchived = service.unarchive(found.id)?;
151            println!("Unarchived account: {}", unarchived.name);
152        }
153    }
154
155    Ok(())
156}