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