Skip to main content

bank_statement_rs/
types.rs

1use crate::{builder::ParsedTransaction, errors::StatementParseError, parsers::qfx::prelude::*};
2use chrono::NaiveDate;
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Transaction {
8    pub date: NaiveDate,
9    pub amount: Decimal,
10    pub payee: Option<String>,
11    pub transaction_type: String,
12    pub fitid: Option<String>,
13    pub status: Option<String>,
14    pub memo: Option<String>,
15}
16
17impl TryFrom<ParsedTransaction> for Transaction {
18    type Error = StatementParseError;
19
20    fn try_from(parsed: ParsedTransaction) -> Result<Self, Self::Error> {
21        match parsed {
22            ParsedTransaction::Qfx(qfx) => qfx.try_into(),
23        }
24    }
25}
26
27impl TryFrom<QfxTransaction> for Transaction {
28    type Error = StatementParseError;
29
30    fn try_from(stmt: QfxTransaction) -> Result<Self, Self::Error> {
31        Ok(Transaction {
32            date: stmt.dt_posted.try_into()?,
33            amount: stmt.amount,
34            payee: stmt.name,
35            transaction_type: stmt.trn_type,
36            fitid: stmt.fitid,
37            status: None,
38            memo: stmt.memo,
39        })
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use chrono::NaiveDate;
47    use rstest::rstest;
48    use rust_decimal::Decimal;
49    use std::str::FromStr;
50
51    fn create_test_qfx_transaction() -> QfxTransaction {
52        QfxTransaction {
53            trn_type: "DEBIT".to_string(),
54            dt_posted: "20251226120000".into(),
55            amount: Decimal::from_str("-50.00").unwrap(),
56            fitid: Some("202512260".to_string()),
57            name: Some("Test Payee".to_string()),
58            memo: Some("Test memo".to_string()),
59        }
60    }
61
62    #[rstest]
63    #[case(
64        "DEBIT",
65        "20251226120000",
66        "-50.00",
67        Some("202512260".to_string()),
68        Some("Test Payee".to_string()),
69        Some("Test memo".to_string()),
70        true
71    )]
72    #[case("CREDIT", "20251225000000", "1500.00", None, None, None, true)]
73    #[case(
74        "DEBIT",
75        "invalid_date",
76        "-50.00",
77        None,
78        None,
79        None,
80        false  // Should fail due to invalid date
81    )]
82    fn test_transaction_from_qfx_transaction(
83        #[case] trn_type: &str,
84        #[case] dt_posted: &str,
85        #[case] amount: &str,
86        #[case] fitid: Option<String>,
87        #[case] name: Option<String>,
88        #[case] memo: Option<String>,
89        #[case] should_succeed: bool,
90    ) {
91        let qfx = QfxTransaction {
92            trn_type: trn_type.to_string(),
93            dt_posted: dt_posted.into(),
94            amount: Decimal::from_str(amount).unwrap(),
95            fitid: fitid.clone(),
96            name: name.clone(),
97            memo: memo.clone(),
98        };
99
100        let result: Result<Transaction, _> = qfx.try_into();
101
102        if should_succeed {
103            assert!(result.is_ok());
104            let transaction = result.unwrap();
105            assert_eq!(transaction.transaction_type, trn_type);
106            assert_eq!(transaction.amount, Decimal::from_str(amount).unwrap());
107            assert_eq!(transaction.payee, name);
108            assert_eq!(transaction.fitid, fitid);
109            assert_eq!(transaction.memo, memo);
110            assert_eq!(transaction.status, None);
111        } else {
112            assert!(result.is_err());
113        }
114    }
115
116    #[test]
117    fn test_transaction_from_parsed_transaction() {
118        let qfx = create_test_qfx_transaction();
119        let parsed = ParsedTransaction::Qfx(qfx);
120
121        let result: Result<Transaction, _> = parsed.try_into();
122        assert!(result.is_ok());
123
124        let transaction = result.unwrap();
125        assert_eq!(transaction.transaction_type, "DEBIT");
126        assert_eq!(transaction.amount, Decimal::from_str("-50.00").unwrap());
127    }
128
129    #[test]
130    fn test_transaction_serialization() {
131        let transaction = Transaction {
132            date: NaiveDate::from_ymd_opt(2025, 12, 26).unwrap(),
133            amount: Decimal::from_str("-50.00").unwrap(),
134            payee: Some("Test Payee".to_string()),
135            transaction_type: "DEBIT".to_string(),
136            fitid: Some("202512260".to_string()),
137            status: None,
138            memo: Some("Test memo".to_string()),
139        };
140
141        let json = serde_json::to_string(&transaction).unwrap();
142        assert!(json.contains("Test Payee"));
143        assert!(json.contains("DEBIT"));
144
145        let deserialized: Transaction = serde_json::from_str(&json).unwrap();
146        assert_eq!(deserialized.payee, transaction.payee);
147        assert_eq!(deserialized.amount, transaction.amount);
148    }
149}