summaryrefslogtreecommitdiff
path: root/src/parser/bmo.rs
blob: bbbe232f00a953d3ef2a00de65696115238b65d9 (plain)
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);
    }
}