rustledger_plugin/native/plugins/
rx_txn.rs1use crate::types::{DirectiveData, MetaValueData, PluginInput, PluginOp, PluginOutput};
7
8use super::super::NativePlugin;
9
10const TAG_RX: &str = "rx_txn";
12
13pub struct RxTxnPlugin;
20
21impl NativePlugin for RxTxnPlugin {
22 fn name(&self) -> &'static str {
23 "rx_txn_plugin"
24 }
25
26 fn description(&self) -> &'static str {
27 "Set default metadata for Regular Expected Transactions (beanahead)"
28 }
29
30 fn process(&self, input: PluginInput) -> PluginOutput {
31 let mut ops: Vec<PluginOp> = Vec::with_capacity(input.directives.len());
32 for (i, mut wrapper) in input.directives.into_iter().enumerate() {
33 let needs_change = matches!(&wrapper.data, DirectiveData::Transaction(t)
34 if t.tags.contains(&TAG_RX.to_string())
35 && (!t.metadata.iter().any(|(k, _)| k == "final")
36 || !t.metadata.iter().any(|(k, _)| k == "roll")));
37
38 if !needs_change {
39 ops.push(PluginOp::Keep(i));
40 continue;
41 }
42
43 if let DirectiveData::Transaction(ref mut txn) = wrapper.data {
44 let has_final = txn.metadata.iter().any(|(k, _)| k == "final");
45 let has_roll = txn.metadata.iter().any(|(k, _)| k == "roll");
46
47 if !has_final {
48 txn.metadata.push((
49 "final".to_string(),
50 MetaValueData::String("None".to_string()),
51 ));
52 }
53 if !has_roll {
54 txn.metadata.push((
55 "roll".to_string(),
56 MetaValueData::String("True".to_string()),
57 ));
58 }
59 }
60 ops.push(PluginOp::Modify(i, wrapper));
61 }
62
63 PluginOutput {
64 ops,
65 errors: Vec::new(),
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::super::utils::materialize_ops;
73 use super::*;
74 use crate::types::*;
75
76 fn create_test_transaction(tags: Vec<&str>, metadata: Vec<(&str, &str)>) -> DirectiveWrapper {
77 DirectiveWrapper {
78 directive_type: "transaction".to_string(),
79 date: "2024-01-15".to_string(),
80 filename: None,
81 lineno: None,
82 data: DirectiveData::Transaction(TransactionData {
83 flag: "*".to_string(),
84 payee: Some("Test".to_string()),
85 narration: "Test transaction".to_string(),
86 tags: tags.into_iter().map(String::from).collect(),
87 links: vec![],
88 metadata: metadata
89 .into_iter()
90 .map(|(k, v)| (k.to_string(), MetaValueData::String(v.to_string())))
91 .collect(),
92 postings: vec![
93 PostingData {
94 account: "Assets:Cash".to_string(),
95 units: Some(AmountData {
96 number: "-100.00".to_string(),
97 currency: "USD".to_string(),
98 }),
99 cost: None,
100 price: None,
101 flag: None,
102 metadata: vec![],
103 },
104 PostingData {
105 account: "Expenses:Test".to_string(),
106 units: Some(AmountData {
107 number: "100.00".to_string(),
108 currency: "USD".to_string(),
109 }),
110 cost: None,
111 price: None,
112 flag: None,
113 metadata: vec![],
114 },
115 ],
116 }),
117 }
118 }
119
120 #[test]
121 fn test_rx_txn_adds_default_metadata() {
122 let plugin = RxTxnPlugin;
123
124 let input = PluginInput {
125 directives: vec![create_test_transaction(vec!["rx_txn"], vec![])],
126 options: PluginOptions {
127 operating_currencies: vec!["USD".to_string()],
128 title: None,
129 },
130 config: None,
131 };
132
133 let input_dirs = input.directives.clone();
134 let output = plugin.process(input);
135 assert_eq!(output.errors.len(), 0);
136 let directives = materialize_ops(&input_dirs, &output);
137 assert_eq!(directives.len(), 1);
138
139 if let DirectiveData::Transaction(txn) = &directives[0].data {
140 let has_final = txn.metadata.iter().any(|(k, _)| k == "final");
141 let has_roll = txn.metadata.iter().any(|(k, _)| k == "roll");
142 assert!(has_final, "Should have 'final' metadata");
143 assert!(has_roll, "Should have 'roll' metadata");
144 } else {
145 panic!("Expected transaction");
146 }
147 }
148
149 #[test]
150 fn test_rx_txn_preserves_existing_metadata() {
151 let plugin = RxTxnPlugin;
152
153 let input = PluginInput {
154 directives: vec![create_test_transaction(
155 vec!["rx_txn"],
156 vec![("final", "2024-12-31"), ("roll", "False")],
157 )],
158 options: PluginOptions {
159 operating_currencies: vec!["USD".to_string()],
160 title: None,
161 },
162 config: None,
163 };
164
165 let input_dirs = input.directives.clone();
166 let output = plugin.process(input);
167 assert_eq!(output.errors.len(), 0);
168 let directives = materialize_ops(&input_dirs, &output);
169
170 if let DirectiveData::Transaction(txn) = &directives[0].data {
171 assert_eq!(txn.metadata.len(), 2);
173 let final_meta = txn.metadata.iter().find(|(k, _)| k == "final").unwrap();
174 if let MetaValueData::String(v) = &final_meta.1 {
175 assert_eq!(v, "2024-12-31");
176 } else {
177 panic!("Expected string metadata value");
178 }
179 } else {
180 panic!("Expected transaction");
181 }
182 }
183
184 #[test]
185 fn test_rx_txn_ignores_untagged_transactions() {
186 let plugin = RxTxnPlugin;
187
188 let input = PluginInput {
189 directives: vec![create_test_transaction(vec![], vec![])],
190 options: PluginOptions {
191 operating_currencies: vec!["USD".to_string()],
192 title: None,
193 },
194 config: None,
195 };
196
197 let input_dirs = input.directives.clone();
198 let output = plugin.process(input);
199 assert_eq!(output.errors.len(), 0);
200 let directives = materialize_ops(&input_dirs, &output);
201
202 if let DirectiveData::Transaction(txn) = &directives[0].data {
203 assert!(txn.metadata.is_empty());
205 } else {
206 panic!("Expected transaction");
207 }
208 }
209
210 #[test]
211 fn test_rx_txn_ignores_other_tags() {
212 let plugin = RxTxnPlugin;
213
214 let input = PluginInput {
215 directives: vec![create_test_transaction(vec!["other_tag"], vec![])],
216 options: PluginOptions {
217 operating_currencies: vec!["USD".to_string()],
218 title: None,
219 },
220 config: None,
221 };
222
223 let input_dirs = input.directives.clone();
224 let output = plugin.process(input);
225 assert_eq!(output.errors.len(), 0);
226 let directives = materialize_ops(&input_dirs, &output);
227
228 if let DirectiveData::Transaction(txn) = &directives[0].data {
229 assert!(txn.metadata.is_empty());
231 } else {
232 panic!("Expected transaction");
233 }
234 }
235}