tackler_core/model/
transaction.rs1use crate::model::{Posts, posting};
7use crate::tackler;
8use jiff::tz;
9use std::cmp::Ordering;
10use std::fmt::Write;
11use std::fmt::{Display, Formatter};
12use tackler_api::txn_header::TxnHeader;
13use tackler_api::txn_ts;
14
15#[derive(Debug, Default)]
16pub struct Transaction {
17 pub(crate) header: TxnHeader,
18 pub(crate) posts: Posts,
19}
20
21impl Transaction {
22 pub fn try_from(header: TxnHeader, posts: Posts) -> Result<Transaction, tackler::Error> {
25 let txn_sum = posting::txn_sum(&posts);
26 if !txn_sum.is_zero() {
27 let msg = format!("TXN postings do not zero: {txn_sum}");
28 return Err(msg.into());
29 }
30
31 Ok(Transaction { header, posts })
32 }
33}
34
35#[must_use]
36pub fn ord_by_txn(before: &Transaction, after: &Transaction) -> Ordering {
37 before.cmp(after)
38}
39
40impl Eq for Transaction {}
41
42impl Ord for Transaction {
43 fn cmp(&self, other: &Self) -> Ordering {
44 self.header.cmp(&other.header)
45 }
46}
47
48impl PartialOrd for Transaction {
49 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
50 Some(self.cmp(other))
51 }
52}
53
54impl PartialEq<Self> for Transaction {
55 fn eq(&self, other: &Self) -> bool {
56 self.header == other.header
57 }
58}
59
60impl Display for Transaction {
61 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62 let indent = " ";
63 write!(
64 f,
65 "{}{}",
66 self.header.to_string_with_indent(
67 indent,
68 |ts, _tz| { txn_ts::rfc_3339(ts) },
69 tz::TimeZone::UTC
70 ),
71 self.posts
72 .iter()
73 .fold(String::with_capacity(256), |mut output, p| {
74 let _ = writeln!(output, "{indent}{p}");
75 output
76 })
77 )
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use indoc::indoc;
85 use rust_decimal::Decimal;
86 use std::sync::Arc;
87 use tackler_rs::IndocUtils;
88
89 use crate::model::TxnAccount;
90 use crate::model::{AccountTreeNode, Commodity, Posting};
91 use tackler_api::txn_header::TxnHeader;
92 use tackler_api::txn_ts::rfc3339_to_zoned;
93
94 fn atn2txntn(atn: AccountTreeNode) -> TxnAccount {
95 TxnAccount {
96 atn: Arc::new(atn),
97 comm: Arc::new(Commodity::default()),
98 }
99 }
100
101 #[test]
102 fn txn_to_display() {
103 let ts = rfc3339_to_zoned("2023-02-04T14:03:05.047974+02:00").unwrap();
104
105 let tnx_hdr = TxnHeader {
106 timestamp: ts,
107 code: None,
108 description: Some("desc".to_string()),
109 uuid: None,
110 extid: None,
111 location: None,
112 tags: None,
113 comments: None,
114 };
115
116 let atn_ab = AccountTreeNode::from("a:b").unwrap();
117 let atn_cd = AccountTreeNode::from("c:d").unwrap();
118 let atn_ef = AccountTreeNode::from("e:f").unwrap();
119
120 let ef_post = Posting::from(
121 atn2txntn(atn_ef),
122 Decimal::from_str_exact("1").unwrap(),
123 Decimal::from_str_exact("0").unwrap(),
124 false,
125 Arc::new(Commodity::default()),
126 None,
127 )
128 .unwrap();
129 let cd_post = Posting::from(
130 atn2txntn(atn_cd),
131 Decimal::from_str_exact("2").unwrap(),
132 Decimal::from_str_exact("0").unwrap(),
133 false,
134 Arc::new(Commodity::default()),
135 None,
136 )
137 .unwrap();
138 let ab_post = Posting::from(
139 atn2txntn(atn_ab),
140 Decimal::from_str_exact("-3").unwrap(),
141 Decimal::from_str_exact("0").unwrap(),
142 false,
143 Arc::new(Commodity::default()),
144 None,
145 )
146 .unwrap();
147
148 let tests: Vec<(Transaction, String)> = vec![(
149 Transaction {
150 header: tnx_hdr,
151 posts: vec![ef_post, cd_post, ab_post],
152 },
153 indoc!(
154 "|2023-02-04T14:03:05.047974+02:00 'desc
155 | e:f 1
156 | c:d 2
157 | a:b -3
158 |"
159 )
160 .strip_margin(),
161 )];
162
163 let mut count = 0;
164 let should_be_count = tests.len();
165 for t in tests {
166 let txn_hdr_str = format!("{}", t.0);
167 assert_eq!(txn_hdr_str, t.1);
168 count += 1;
169 }
170 assert_eq!(count, should_be_count);
171 }
172}