bank_statement_rs/
types.rs1use 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 )]
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}