tackler_api/filters/posting/
posting_amount_greater.rs

1/*
2 * Tackler-NG 2023-2024
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use jiff::tz::TimeZone;
7use regex::Regex;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use std::fmt::Formatter;
11use tackler_rs::regex::peeled_pattern;
12use tackler_rs::regex::serde::full_haystack_matcher;
13
14use crate::filters::{IndentDisplay, posting_filter_indent_fmt};
15
16/// Txn Posting "Amount is Greater than" filter
17///
18/// Select the transaction, if its posting match `regex` with amount greater than `amount`
19///
20/// Q: Why there is also account regex as parameter?
21///
22/// A: To support negative amounts as an argument.
23///    Sum of all postings inside transaction must be zero.
24///    If you select "more than some negative amount",
25///    then all transactions will match, because there must
26///    be postings with positive amounts in every transaction
27///    to zero out the whole transaction.
28///    Hence the filter would be useless without account selector.
29#[derive(Serialize, Deserialize, Clone, Debug)]
30pub struct TxnFilterPostingAmountGreater {
31    #[doc(hidden)]
32    #[serde(with = "full_haystack_matcher")]
33    pub regex: Regex,
34    #[doc(hidden)]
35    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
36    pub amount: Decimal,
37}
38
39impl IndentDisplay for TxnFilterPostingAmountGreater {
40    fn i_fmt(&self, indent: &str, _tz: TimeZone, f: &mut Formatter<'_>) -> std::fmt::Result {
41        posting_filter_indent_fmt(
42            indent,
43            "Posting Amount",
44            peeled_pattern(&self.regex),
45            ">",
46            &self.amount,
47            f,
48        )
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::filters::{
56        FilterDefZoned, FilterDefinition, NullaryTRUE, TxnFilter, logic::TxnFilterAND,
57    };
58    use indoc::indoc;
59    use jiff::tz;
60    use tackler_rs::IndocUtils;
61    use tackler_rs::regex::new_full_haystack_regex;
62
63    #[test]
64    // test: 8609eb58-f600-42d1-a20a-c7de2c57e6e2
65    // desc: PostingAmountGreater, full haystack match
66    fn posting_amount_greater_full_haystack() {
67        let filter_json_str =
68            r#"{"txnFilter":{"TxnFilterPostingAmountGreater":{"regex":"o.a","amount":1}}}"#;
69
70        let tf_res = serde_json::from_str::<FilterDefinition>(filter_json_str);
71        assert!(tf_res.is_ok());
72        let tf = tf_res.unwrap(/*:test:*/);
73
74        if let TxnFilter::TxnFilterPostingAmountGreater(f) = &tf.txn_filter {
75            assert!(!f.regex.is_match("foobar"));
76            assert!(!f.regex.is_match("obar"));
77            assert!(!f.regex.is_match("ooba"));
78
79            assert!(f.regex.is_match("oba"));
80        } else {
81            panic!(/*:test:*/)
82        }
83    }
84
85    #[test]
86    // test: 66d6ee10-a18e-4615-9e7a-1569c793fe46
87    // desc: PostingAmountGreater, JSON
88    fn posting_amount_greater_json() {
89        let filter_json_str = r#"{"txnFilter":{"TxnFilterPostingAmountGreater":{"regex":"(abc.*)|(def.*)","amount":1}}}"#;
90
91        let filter_text_str = indoc! {
92        r#"|Filter
93           |  Posting Amount
94           |    account: "(abc.*)|(def.*)"
95           |    amount > 1
96           |"#}
97        .strip_margin();
98
99        let tf_res = serde_json::from_str::<FilterDefinition>(filter_json_str);
100        assert!(tf_res.is_ok());
101        let tf = tf_res.unwrap(/*:test:*/);
102
103        if let TxnFilter::TxnFilterPostingAmountGreater(_) = tf.txn_filter {
104        } else {
105            panic!(/*:test:*/)
106        }
107
108        assert_eq!(
109            format!(
110                "{}",
111                FilterDefZoned {
112                    filt_def: &tf,
113                    tz: tz::TimeZone::UTC
114                }
115            ),
116            filter_text_str
117        );
118        assert_eq!(
119            serde_json::to_string(&tf).unwrap(/*:test:*/),
120            filter_json_str
121        );
122    }
123
124    #[test]
125    // test: f940a623-f4b6-4937-86ff-c05ddc1921d6
126    // desc: PostingAmountGreater, Text
127    fn posting_amount_greater_text() {
128        let filter_text_str = indoc! {
129        r#"|Filter
130           |  AND
131           |    Posting Amount
132           |      account: "(abc.*)|(def.*)"
133           |      amount > 1
134           |    AND
135           |      Posting Amount
136           |        account: "xyz"
137           |        amount > 2
138           |      All pass
139           |"#}
140        .strip_margin();
141
142        let tf = FilterDefinition {
143            txn_filter: TxnFilter::TxnFilterAND(TxnFilterAND {
144                txn_filters: vec![
145                    TxnFilter::TxnFilterPostingAmountGreater(TxnFilterPostingAmountGreater {
146                        regex: new_full_haystack_regex("(abc.*)|(def.*)").unwrap(/*:test:*/),
147                        amount: Decimal::from(1),
148                    }),
149                    TxnFilter::TxnFilterAND(TxnFilterAND {
150                        txn_filters: vec![
151                            TxnFilter::TxnFilterPostingAmountGreater(
152                                TxnFilterPostingAmountGreater {
153                                    regex: new_full_haystack_regex("xyz").unwrap(/*:test:*/),
154                                    amount: Decimal::from(2),
155                                },
156                            ),
157                            TxnFilter::NullaryTRUE(NullaryTRUE {}),
158                        ],
159                    }),
160                ],
161            }),
162        };
163
164        assert_eq!(
165            format!(
166                "{}",
167                FilterDefZoned {
168                    filt_def: &tf,
169                    tz: tz::TimeZone::UTC
170                }
171            ),
172            filter_text_str
173        );
174    }
175}