envelope_cli/cli/
import.rs1use std::path::Path;
7
8use crate::error::{EnvelopeError, EnvelopeResult};
9use crate::models::{Account, AccountId};
10use crate::services::{
11 AccountService, ImportPreviewEntry, ImportService, ImportStatus, ParsedTransaction,
12};
13use crate::storage::Storage;
14
15pub fn handle_import_command(storage: &Storage, file: &str, account: &str) -> EnvelopeResult<()> {
17 let account_service = AccountService::new(storage);
18 let import_service = ImportService::new(storage);
19
20 let (parsed, target_account) =
21 read_and_parse_csv(&import_service, &account_service, file, account)?;
22
23 if parsed.is_empty() {
24 println!("No transactions found in CSV file.");
25 return Ok(());
26 }
27
28 let preview = generate_and_display_preview(&import_service, &parsed, &target_account)?;
29
30 let new_count = preview
31 .iter()
32 .filter(|e| e.status == ImportStatus::New)
33 .count();
34
35 if new_count > 0 {
36 execute_import(&import_service, &preview, target_account.id)?;
37 }
38
39 Ok(())
40}
41
42fn read_and_parse_csv(
44 import_service: &ImportService,
45 account_service: &AccountService,
46 file: &str,
47 account: &str,
48) -> EnvelopeResult<(Vec<Result<ParsedTransaction, String>>, Account)> {
49 let target_account = account_service
50 .find(account)?
51 .ok_or_else(|| EnvelopeError::account_not_found(account))?;
52
53 let path = Path::new(file);
54 if !path.exists() {
55 return Err(EnvelopeError::Import(format!("File not found: {}", file)));
56 }
57
58 let mut reader = csv::Reader::from_path(path)
59 .map_err(|e| EnvelopeError::Import(format!("Failed to open CSV file: {}", e)))?;
60 let headers = reader
61 .headers()
62 .map_err(|e| EnvelopeError::Import(format!("Failed to read CSV headers: {}", e)))?
63 .clone();
64 let mapping = import_service.detect_mapping_from_headers(&headers);
65
66 let parsed = import_service.parse_csv_from_reader(&mut reader, &mapping)?;
67
68 Ok((parsed, target_account))
69}
70
71fn generate_and_display_preview(
73 import_service: &ImportService,
74 parsed: &[Result<ParsedTransaction, String>],
75 target_account: &Account,
76) -> EnvelopeResult<Vec<ImportPreviewEntry>> {
77 let preview = import_service.generate_preview(parsed, target_account.id)?;
78
79 let new_count = preview
80 .iter()
81 .filter(|e| e.status == ImportStatus::New)
82 .count();
83 let dup_count = preview
84 .iter()
85 .filter(|e| e.status == ImportStatus::Duplicate)
86 .count();
87 let err_count = preview
88 .iter()
89 .filter(|e| matches!(e.status, ImportStatus::Error(_)))
90 .count();
91
92 println!("Import Preview for '{}'", target_account.name);
93 println!("{}", "=".repeat(40));
94 println!(" New transactions: {}", new_count);
95 println!(" Duplicates (skip): {}", dup_count);
96 println!(" Errors: {}", err_count);
97 println!();
98
99 if new_count == 0 {
100 println!("No new transactions to import.");
101 } else {
102 println!("First transactions to import:");
103 for entry in preview
104 .iter()
105 .filter(|e| e.status == ImportStatus::New)
106 .take(5)
107 {
108 println!(
109 " {} {} {}",
110 entry.transaction.date, entry.transaction.payee, entry.transaction.amount
111 );
112 }
113 if new_count > 5 {
114 println!(" ... and {} more", new_count - 5);
115 }
116 println!();
117 }
118
119 Ok(preview)
120}
121
122fn execute_import(
124 import_service: &ImportService,
125 preview: &[ImportPreviewEntry],
126 account_id: AccountId,
127) -> EnvelopeResult<()> {
128 let result = import_service.import_from_preview(
129 preview, account_id, None, false, )?;
132
133 println!("Import Complete!");
134 println!(" Imported: {}", result.imported);
135 println!(" Skipped: {}", result.duplicates_skipped);
136 if !result.error_messages.is_empty() {
137 println!(" Errors: {}", result.errors);
138 for (row, msg) in &result.error_messages {
139 println!(" Row {}: {}", row + 1, msg);
140 }
141 }
142
143 Ok(())
144}