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)
60 .map_err(|e| EnvelopeError::Import(format!("Failed to open CSV file: {}", e)))?;
61 let headers = reader
62 .headers()
63 .map_err(|e| EnvelopeError::Import(format!("Failed to read CSV headers: {}", e)))?
64 .clone();
65 let mapping = import_service.detect_mapping_from_headers(&headers);
66
67 let parsed = if !mapping.has_header {
69 let mut reader = csv::ReaderBuilder::new()
70 .has_headers(false)
71 .from_path(path)
72 .map_err(|e| EnvelopeError::Import(format!("Failed to open CSV file: {}", e)))?;
73 import_service.parse_csv_from_reader(&mut reader, &mapping)?
74 } else {
75 import_service.parse_csv_from_reader(&mut reader, &mapping)?
76 };
77
78 Ok((parsed, target_account))
79}
80
81fn generate_and_display_preview(
83 import_service: &ImportService,
84 parsed: &[Result<ParsedTransaction, String>],
85 target_account: &Account,
86) -> EnvelopeResult<Vec<ImportPreviewEntry>> {
87 let preview = import_service.generate_preview(parsed, target_account.id)?;
88
89 let new_count = preview
90 .iter()
91 .filter(|e| e.status == ImportStatus::New)
92 .count();
93 let dup_count = preview
94 .iter()
95 .filter(|e| e.status == ImportStatus::Duplicate)
96 .count();
97 let err_count = preview
98 .iter()
99 .filter(|e| matches!(e.status, ImportStatus::Error(_)))
100 .count();
101
102 println!("Import Preview for '{}'", target_account.name);
103 println!("{}", "=".repeat(40));
104 println!(" New transactions: {}", new_count);
105 println!(" Duplicates (skip): {}", dup_count);
106 println!(" Errors: {}", err_count);
107 println!();
108
109 if new_count == 0 {
110 println!("No new transactions to import.");
111 } else {
112 println!("First transactions to import:");
113 for entry in preview
114 .iter()
115 .filter(|e| e.status == ImportStatus::New)
116 .take(5)
117 {
118 println!(
119 " {} {} {}",
120 entry.transaction.date, entry.transaction.payee, entry.transaction.amount
121 );
122 }
123 if new_count > 5 {
124 println!(" ... and {} more", new_count - 5);
125 }
126 println!();
127 }
128
129 Ok(preview)
130}
131
132fn execute_import(
134 import_service: &ImportService,
135 preview: &[ImportPreviewEntry],
136 account_id: AccountId,
137) -> EnvelopeResult<()> {
138 let result = import_service.import_from_preview(
139 preview, account_id, None, false, )?;
142
143 println!("Import Complete!");
144 println!(" Imported: {}", result.imported);
145 println!(" Skipped: {}", result.duplicates_skipped);
146 if !result.error_messages.is_empty() {
147 println!(" Errors: {}", result.errors);
148 for (row, msg) in &result.error_messages {
149 println!(" Row {}: {}", row + 1, msg);
150 }
151 }
152
153 Ok(())
154}