envelope_cli/cli/
income.rs1use 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#[derive(Subcommand)]
15pub enum IncomeCommands {
16 Set {
18 amount: String,
20
21 #[arg(short, long)]
23 period: Option<String>,
24
25 #[arg(short, long)]
27 notes: Option<String>,
28 },
29
30 Show {
32 #[arg(short, long)]
34 period: Option<String>,
35 },
36
37 Remove {
39 #[arg(short, long)]
41 period: Option<String>,
42 },
43
44 Compare {
46 #[arg(short, long)]
48 period: Option<String>,
49 },
50}
51
52pub 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 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 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 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 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}