envelope_cli/cli/
income.rs

1//! Income CLI commands
2//!
3//! Implements CLI commands for managing expected income per budget period.
4
5use clap::Subcommand;
6
7use crate::config::settings::Settings;
8use crate::error::{EnvelopeError, EnvelopeResult};
9use crate::models::Money;
10use crate::services::{BudgetService, IncomeService, PeriodService};
11use crate::storage::Storage;
12
13/// Income subcommands
14#[derive(Subcommand)]
15pub enum IncomeCommands {
16    /// Set expected income for a period
17    Set {
18        /// Expected income amount (e.g., "5000" or "5000.00")
19        amount: String,
20
21        /// Budget period (e.g., "2025-01" for January 2025)
22        #[arg(short, long)]
23        period: Option<String>,
24
25        /// Notes about this income expectation
26        #[arg(short, long)]
27        notes: Option<String>,
28    },
29
30    /// Show expected income for a period
31    Show {
32        /// Budget period (defaults to current month)
33        #[arg(short, long)]
34        period: Option<String>,
35    },
36
37    /// Remove expected income for a period
38    Remove {
39        /// Budget period
40        #[arg(short, long)]
41        period: Option<String>,
42    },
43
44    /// Compare expected income vs budgeted amounts
45    Compare {
46        /// Budget period (defaults to current month)
47        #[arg(short, long)]
48        period: Option<String>,
49    },
50}
51
52/// Handle an income command
53pub fn handle_income_command(
54    storage: &Storage,
55    settings: &Settings,
56    cmd: IncomeCommands,
57) -> EnvelopeResult<()> {
58    let period_service = PeriodService::new(settings);
59    let income_service = IncomeService::new(storage);
60    let budget_service = BudgetService::new(storage);
61
62    match cmd {
63        IncomeCommands::Set {
64            amount,
65            period,
66            notes,
67        } => {
68            let period = period_service.parse_or_current(period.as_deref())?;
69            let amount = Money::parse(&amount)
70                .map_err(|e| EnvelopeError::Validation(format!("Invalid amount: {}", e)))?;
71            let friendly = period_service.format_period_friendly(&period);
72
73            let expectation = income_service.set_expected_income(&period, amount, notes)?;
74
75            println!(
76                "Set expected income for {} to {}",
77                friendly, expectation.expected_amount
78            );
79
80            // Show comparison if budget exists
81            if let Some(overage) = budget_service.is_over_expected_income(&period)? {
82                println!(
83                    "Warning: You're budgeting {} more than expected income!",
84                    overage
85                );
86            } else if let Some(remaining) =
87                budget_service.get_remaining_to_budget_from_income(&period)?
88            {
89                if remaining.is_positive() {
90                    println!("Remaining to budget from income: {}", remaining);
91                } else {
92                    println!("Budget matches expected income.");
93                }
94            }
95        }
96
97        IncomeCommands::Show { period } => {
98            let period = period_service.parse_or_current(period.as_deref())?;
99            let friendly = period_service.format_period_friendly(&period);
100
101            if let Some(expectation) = income_service.get_income_expectation(&period) {
102                println!("Expected Income for {}", friendly);
103                println!("{}", "-".repeat(40));
104                println!("Amount: {}", expectation.expected_amount);
105                if !expectation.notes.is_empty() {
106                    println!("Notes:  {}", expectation.notes);
107                }
108
109                // Show budget comparison
110                let overview = budget_service.get_budget_overview(&period)?;
111                println!();
112                println!("Budget Comparison:");
113                println!("  Expected Income:  {}", expectation.expected_amount);
114                println!("  Total Budgeted:   {}", overview.total_budgeted);
115
116                let diff = expectation.expected_amount - overview.total_budgeted;
117                if diff.is_negative() {
118                    println!("  Over Budget:      {} ⚠", diff.abs());
119                } else {
120                    println!("  Remaining:        {} ✓", diff);
121                }
122            } else {
123                println!("No expected income set for {}", friendly);
124                println!("Use 'envelope income set <amount>' to set expected income.");
125            }
126        }
127
128        IncomeCommands::Remove { period } => {
129            let period = period_service.parse_or_current(period.as_deref())?;
130            let friendly = period_service.format_period_friendly(&period);
131
132            if income_service.delete_expected_income(&period)? {
133                println!("Removed expected income for {}", friendly);
134            } else {
135                println!("No expected income was set for {}", friendly);
136            }
137        }
138
139        IncomeCommands::Compare { period } => {
140            let period = period_service.parse_or_current(period.as_deref())?;
141            let friendly = period_service.format_period_friendly(&period);
142            let overview = budget_service.get_budget_overview(&period)?;
143
144            println!("Income vs Budget Comparison for {}", friendly);
145            println!("{}", "=".repeat(50));
146
147            if let Some(expectation) = income_service.get_income_expectation(&period) {
148                println!("Expected Income:     {:>12}", expectation.expected_amount);
149                println!("Total Budgeted:      {:>12}", overview.total_budgeted);
150                println!("{}", "-".repeat(50));
151
152                let diff = expectation.expected_amount - overview.total_budgeted;
153                if diff.is_negative() {
154                    println!("OVER BUDGET:         {:>12} ⚠", diff.abs());
155                    println!();
156                    println!("Warning: You're budgeting more than you expect to earn!");
157                    println!("Consider reducing budget allocations or increasing expected income.");
158                } else if diff.is_zero() {
159                    println!("Remaining to Budget: {:>12} ✓", diff);
160                    println!();
161                    println!("Your budget exactly matches expected income.");
162                } else {
163                    println!("Remaining to Budget: {:>12} ✓", diff);
164                    println!();
165                    println!("You have {} available to budget.", diff);
166                }
167
168                // Show additional info
169                if !expectation.notes.is_empty() {
170                    println!();
171                    println!("Notes: {}", expectation.notes);
172                }
173            } else {
174                println!("Expected Income:     Not set");
175                println!("Total Budgeted:      {:>12}", overview.total_budgeted);
176                println!();
177                println!("Tip: Set expected income with 'envelope income set <amount>'");
178            }
179
180            // Show available to budget (from account balances)
181            println!();
182            println!("{}", "-".repeat(50));
183            println!(
184                "Available to Budget (from accounts): {:>12}",
185                overview.available_to_budget
186            );
187        }
188    }
189
190    Ok(())
191}