1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
use crate::model::Transaction;
use chrono::NaiveDate;
pub fn parse(filename: &str) -> anyhow::Result<Vec<Transaction>> {
let content = std::fs::read_to_string(filename)?;
// Find the line that starts with "Item #" to locate the actual CSV data
let lines: Vec<&str> = content.lines().collect();
let mut csv_start = 0;
for (i, line) in lines.iter().enumerate() {
if line.starts_with("Item #") {
csv_start = i;
break;
}
}
// Create CSV content from the found start point
let csv_content = lines[csv_start..].join("\n");
let mut reader = csv::ReaderBuilder::new()
.has_headers(true)
.from_reader(csv_content.as_bytes());
let mut transactions = Vec::new();
for result in reader.records() {
let record = result?;
if record.len() >= 6 {
// Format: Item #, Card #, Transaction Date, Posting Date, Transaction Amount, Description
if let (Ok(date), Ok(amount), Some(description)) = (
NaiveDate::parse_from_str(&record[2], "%Y%m%d"),
record[4].parse::<f64>(),
record.get(5),
) {
// BMO amounts are positive for charges, negative for payments
let account = "BMO Mastercard".to_string();
transactions.push(Transaction::new(
date,
description.to_string(),
-amount,
account,
));
}
}
}
Ok(transactions)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_bmo_parser() {
let csv_content = "Following data is valid as of 20250619015709:\n\nItem #,Card #,Transaction Date,Posting Date,Transaction Amount,Description\n1,'5191230194633468',20250612,20250616,7.5,AHS ACH PARKING LOTS EDMONTON AB\n2,'5191230194633468',20250615,20250616,144.9,SP MODERN K9 LTD CALGARY AB";
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(csv_content.as_bytes()).unwrap();
temp_file.flush().unwrap();
let transactions = parse(temp_file.path().to_str().unwrap()).unwrap();
assert_eq!(transactions.len(), 2);
let first_txn = &transactions[0];
assert_eq!(first_txn.date.format("%Y-%m-%d").to_string(), "2025-06-12");
assert_eq!(first_txn.description, "AHS ACH PARKING LOTS EDMONTON AB");
assert_eq!(first_txn.amount, -7.5);
assert_eq!(first_txn.account, "BMO Mastercard");
assert_eq!(first_txn.category, Some("Transportation".to_string()));
let second_txn = &transactions[1];
assert_eq!(second_txn.date.format("%Y-%m-%d").to_string(), "2025-06-15");
assert_eq!(second_txn.description, "SP MODERN K9 LTD CALGARY AB");
assert_eq!(second_txn.amount, -144.9);
assert_eq!(second_txn.account, "BMO Mastercard");
assert_eq!(second_txn.category, Some("Other".to_string()));
}
#[test]
fn test_bmo_parser_with_payment() {
let csv_content = "Following data is valid as of 20250619015709:\n\nItem #,Card #,Transaction Date,Posting Date,Transaction Amount,Description\n1,'5191230194633468',20250604,20250604,-1168.15,AUTOMATIC PYMT RECEIVED";
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(csv_content.as_bytes()).unwrap();
temp_file.flush().unwrap();
let transactions = parse(temp_file.path().to_str().unwrap()).unwrap();
assert_eq!(transactions.len(), 1);
let payment_txn = &transactions[0];
assert_eq!(
payment_txn.date.format("%Y-%m-%d").to_string(),
"2025-06-04"
);
assert_eq!(payment_txn.description, "AUTOMATIC PYMT RECEIVED");
assert_eq!(payment_txn.amount, 1168.15); // Payment becomes positive
assert_eq!(payment_txn.account, "BMO Mastercard");
assert_eq!(payment_txn.category, Some("Transfer".to_string()));
}
#[test]
fn test_bmo_parser_no_item_header() {
let csv_content = "Just some text without Item # header\nThis should not parse anything";
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(csv_content.as_bytes()).unwrap();
temp_file.flush().unwrap();
let transactions = parse(temp_file.path().to_str().unwrap()).unwrap();
assert_eq!(transactions.len(), 0);
}
}
|