1use chrono::NaiveDate;
6use clap::Subcommand;
7
8use crate::error::{EnvelopeError, EnvelopeResult};
9use crate::models::Money;
10use crate::services::{AccountService, CategoryService, ReconciliationService};
11use crate::storage::Storage;
12
13#[derive(Subcommand)]
15pub enum ReconcileCommands {
16 Start {
18 account: String,
20 balance: String,
22 #[arg(short, long)]
24 date: Option<String>,
25 },
26 Status {
28 account: String,
30 balance: String,
32 #[arg(short, long)]
34 date: Option<String>,
35 },
36 Clear {
38 id: String,
40 },
41 Unclear {
43 id: String,
45 },
46 Complete {
48 account: String,
50 balance: String,
52 #[arg(short, long)]
54 date: Option<String>,
55 },
56 Adjust {
58 account: String,
60 balance: String,
62 #[arg(short, long)]
64 date: Option<String>,
65 #[arg(short, long)]
67 category: Option<String>,
68 },
69}
70
71pub fn handle_reconcile_command(storage: &Storage, cmd: ReconcileCommands) -> EnvelopeResult<()> {
73 let service = ReconciliationService::new(storage);
74 let account_service = AccountService::new(storage);
75 let category_service = CategoryService::new(storage);
76
77 match cmd {
78 ReconcileCommands::Start {
79 account,
80 balance,
81 date,
82 } => {
83 let account = account_service
84 .find(&account)?
85 .ok_or_else(|| EnvelopeError::account_not_found(&account))?;
86
87 let statement_balance = Money::parse(&balance).map_err(|e| {
88 EnvelopeError::Validation(format!(
89 "Invalid balance format: '{}'. Use format like '1234.56'. Error: {}",
90 balance, e
91 ))
92 })?;
93
94 let statement_date = parse_date_or_today(date.as_deref())?;
95
96 let session = service.start(account.id, statement_date, statement_balance)?;
97 let summary = service.get_summary(&session)?;
98
99 println!("Reconciliation started for: {}", account.name);
100 println!("Statement Date: {}", statement_date);
101 println!("Statement Balance: {}", statement_balance);
102 println!();
103 println!("Current Status:");
104 println!(
105 " Starting reconciled balance: {}",
106 session.starting_cleared_balance
107 );
108 println!(
109 " Current cleared balance: {}",
110 summary.current_cleared_balance
111 );
112 println!(" Difference: {}", summary.difference);
113 println!();
114 println!("Transactions:");
115 println!(
116 " Cleared (ready to reconcile): {}",
117 summary.cleared_transactions.len()
118 );
119 println!(
120 " Pending (uncleared): {}",
121 summary.uncleared_transactions.len()
122 );
123
124 if !summary.cleared_transactions.is_empty() {
125 println!();
126 println!("Cleared transactions:");
127 for txn in &summary.cleared_transactions {
128 println!(
129 " {} {} {:>12} {}",
130 txn.id.to_string().chars().take(8).collect::<String>(),
131 txn.date,
132 txn.amount,
133 txn.payee_name
134 );
135 }
136 }
137
138 if !summary.uncleared_transactions.is_empty() {
139 println!();
140 println!("Pending transactions:");
141 for txn in &summary.uncleared_transactions {
142 println!(
143 " {} {} {:>12} {}",
144 txn.id.to_string().chars().take(8).collect::<String>(),
145 txn.date,
146 txn.amount,
147 txn.payee_name
148 );
149 }
150 }
151
152 println!();
153 if summary.can_complete {
154 println!("Ready to complete! Run 'envelope reconcile complete' to finish.");
155 } else {
156 println!(
157 "Difference is {}. Clear/unclear transactions until difference is $0.00",
158 summary.difference
159 );
160 println!("Or use 'envelope reconcile adjust' to create an adjustment transaction.");
161 }
162 }
163
164 ReconcileCommands::Status {
165 account,
166 balance,
167 date,
168 } => {
169 let account = account_service
170 .find(&account)?
171 .ok_or_else(|| EnvelopeError::account_not_found(&account))?;
172
173 let statement_balance = Money::parse(&balance)
174 .map_err(|e| EnvelopeError::Validation(format!("Invalid balance: {}", e)))?;
175
176 let statement_date = parse_date_or_today(date.as_deref())?;
177
178 let session = service.start(account.id, statement_date, statement_balance)?;
179 let summary = service.get_summary(&session)?;
180
181 println!("Reconciliation Status: {}", account.name);
182 println!("{}", "=".repeat(40));
183 println!();
184 println!("Statement balance: {}", statement_balance);
185 println!("Current cleared: {}", summary.current_cleared_balance);
186 println!("Difference: {}", summary.difference);
187 println!();
188
189 if let Some(last_date) = account.last_reconciled_date {
190 println!("Last reconciliation: {}", last_date);
191 if let Some(last_balance) = account.last_reconciled_balance {
192 println!("Last reconciled balance: {}", last_balance);
193 }
194 } else {
195 println!("Last reconciliation: Never");
196 }
197 }
198
199 ReconcileCommands::Clear { id } => {
200 let txn = service.clear_transaction(id.parse().map_err(|_| {
201 EnvelopeError::Validation(format!("Invalid transaction ID: {}", id))
202 })?)?;
203
204 println!("Cleared: {} {} {}", txn.date, txn.payee_name, txn.amount);
205 }
206
207 ReconcileCommands::Unclear { id } => {
208 let txn = service.unclear_transaction(id.parse().map_err(|_| {
209 EnvelopeError::Validation(format!("Invalid transaction ID: {}", id))
210 })?)?;
211
212 println!("Uncleared: {} {} {}", txn.date, txn.payee_name, txn.amount);
213 }
214
215 ReconcileCommands::Complete {
216 account,
217 balance,
218 date,
219 } => {
220 let account = account_service
221 .find(&account)?
222 .ok_or_else(|| EnvelopeError::account_not_found(&account))?;
223
224 let statement_balance = Money::parse(&balance)
225 .map_err(|e| EnvelopeError::Validation(format!("Invalid balance: {}", e)))?;
226
227 let statement_date = parse_date_or_today(date.as_deref())?;
228
229 let session = service.start(account.id, statement_date, statement_balance)?;
230 let result = service.complete(&session)?;
231
232 println!("Reconciliation complete!");
233 println!(" Account: {}", account.name);
234 println!(" Statement date: {}", statement_date);
235 println!(" Statement balance: {}", statement_balance);
236 println!(
237 " Transactions reconciled: {}",
238 result.transactions_reconciled
239 );
240 }
241
242 ReconcileCommands::Adjust {
243 account,
244 balance,
245 date,
246 category,
247 } => {
248 let account = account_service
249 .find(&account)?
250 .ok_or_else(|| EnvelopeError::account_not_found(&account))?;
251
252 let statement_balance = Money::parse(&balance)
253 .map_err(|e| EnvelopeError::Validation(format!("Invalid balance: {}", e)))?;
254
255 let statement_date = parse_date_or_today(date.as_deref())?;
256
257 let category_id = if let Some(cat_name) = category {
258 let cat = category_service
259 .find_category(&cat_name)?
260 .ok_or_else(|| EnvelopeError::category_not_found(&cat_name))?;
261 Some(cat.id)
262 } else {
263 None
264 };
265
266 let session = service.start(account.id, statement_date, statement_balance)?;
267 let summary = service.get_summary(&session)?;
268
269 println!(
270 "Creating adjustment transaction for: {}",
271 summary.difference
272 );
273
274 let result = service.complete_with_adjustment(&session, category_id)?;
275
276 println!();
277 println!("Reconciliation complete with adjustment!");
278 println!(" Account: {}", account.name);
279 println!(" Statement date: {}", statement_date);
280 println!(" Statement balance: {}", statement_balance);
281 println!(
282 " Transactions reconciled: {}",
283 result.transactions_reconciled
284 );
285 if result.adjustment_created {
286 println!(
287 " Adjustment created: {}",
288 result.adjustment_amount.unwrap()
289 );
290 }
291 }
292 }
293
294 Ok(())
295}
296
297fn parse_date_or_today(date_str: Option<&str>) -> EnvelopeResult<NaiveDate> {
299 if let Some(date_str) = date_str {
300 NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| {
301 EnvelopeError::Validation(format!(
302 "Invalid date format: '{}'. Use YYYY-MM-DD",
303 date_str
304 ))
305 })
306 } else {
307 Ok(chrono::Local::now().date_naive())
308 }
309}