rustledger_plugin/native/plugins/
rx_txn.rs1use crate::types::{DirectiveData, MetaValueData, PluginInput, PluginOp, PluginOutput};
7
8use super::super::{NativePlugin, RegularPlugin};
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
70impl RegularPlugin for RxTxnPlugin {}
71
72#[cfg(test)]
73mod tests {
74 use super::super::utils::materialize_ops;
75 use super::*;
76 use crate::types::*;
77
78 fn create_test_transaction(tags: Vec<&str>, metadata: Vec<(&str, &str)>) -> DirectiveWrapper {
79 DirectiveWrapper {
80 directive_type: "transaction".to_string(),
81 date: "2024-01-15".to_string(),
82 filename: None,
83 lineno: None,
84 data: DirectiveData::Transaction(TransactionData {
85 flag: "*".to_string(),
86 payee: Some("Test".to_string()),
87 narration: "Test transaction".to_string(),
88 tags: tags.into_iter().map(String::from).collect(),
89 links: vec![],
90 metadata: metadata
91 .into_iter()
92 .map(|(k, v)| (k.to_string(), MetaValueData::String(v.to_string())))
93 .collect(),
94 postings: vec![
95 PostingData {
96 account: "Assets:Cash".to_string(),
97 units: Some(AmountData {
98 number: "-100.00".to_string(),
99 currency: "USD".to_string(),
100 }),
101 cost: None,
102 price: None,
103 flag: None,
104 metadata: vec![],
105 span: None,
106 },
107 PostingData {
108 account: "Expenses:Test".to_string(),
109 units: Some(AmountData {
110 number: "100.00".to_string(),
111 currency: "USD".to_string(),
112 }),
113 cost: None,
114 price: None,
115 flag: None,
116 metadata: vec![],
117 span: None,
118 },
119 ],
120 }),
121 }
122 }
123
124 #[test]
125 fn test_rx_txn_adds_default_metadata() {
126 let plugin = RxTxnPlugin;
127
128 let input = PluginInput {
129 directives: vec![create_test_transaction(vec!["rx_txn"], vec![])],
130 options: PluginOptions {
131 operating_currencies: vec!["USD".to_string()],
132 title: None,
133 },
134 config: None,
135 };
136
137 let input_dirs = input.directives.clone();
138 let output = plugin.process(input);
139 assert_eq!(output.errors.len(), 0);
140 let directives = materialize_ops(&input_dirs, &output);
141 assert_eq!(directives.len(), 1);
142
143 if let DirectiveData::Transaction(txn) = &directives[0].data {
144 let has_final = txn.metadata.iter().any(|(k, _)| k == "final");
145 let has_roll = txn.metadata.iter().any(|(k, _)| k == "roll");
146 assert!(has_final, "Should have 'final' metadata");
147 assert!(has_roll, "Should have 'roll' metadata");
148 } else {
149 panic!("Expected transaction");
150 }
151 }
152
153 #[test]
154 fn test_rx_txn_preserves_existing_metadata() {
155 let plugin = RxTxnPlugin;
156
157 let input = PluginInput {
158 directives: vec![create_test_transaction(
159 vec!["rx_txn"],
160 vec![("final", "2024-12-31"), ("roll", "False")],
161 )],
162 options: PluginOptions {
163 operating_currencies: vec!["USD".to_string()],
164 title: None,
165 },
166 config: None,
167 };
168
169 let input_dirs = input.directives.clone();
170 let output = plugin.process(input);
171 assert_eq!(output.errors.len(), 0);
172 let directives = materialize_ops(&input_dirs, &output);
173
174 if let DirectiveData::Transaction(txn) = &directives[0].data {
175 assert_eq!(txn.metadata.len(), 2);
177 let final_meta = txn.metadata.iter().find(|(k, _)| k == "final").unwrap();
178 if let MetaValueData::String(v) = &final_meta.1 {
179 assert_eq!(v, "2024-12-31");
180 } else {
181 panic!("Expected string metadata value");
182 }
183 } else {
184 panic!("Expected transaction");
185 }
186 }
187
188 #[test]
189 fn test_rx_txn_ignores_untagged_transactions() {
190 let plugin = RxTxnPlugin;
191
192 let input = PluginInput {
193 directives: vec![create_test_transaction(vec![], vec![])],
194 options: PluginOptions {
195 operating_currencies: vec!["USD".to_string()],
196 title: None,
197 },
198 config: None,
199 };
200
201 let input_dirs = input.directives.clone();
202 let output = plugin.process(input);
203 assert_eq!(output.errors.len(), 0);
204 let directives = materialize_ops(&input_dirs, &output);
205
206 if let DirectiveData::Transaction(txn) = &directives[0].data {
207 assert!(txn.metadata.is_empty());
209 } else {
210 panic!("Expected transaction");
211 }
212 }
213
214 #[test]
215 fn test_rx_txn_ignores_other_tags() {
216 let plugin = RxTxnPlugin;
217
218 let input = PluginInput {
219 directives: vec![create_test_transaction(vec!["other_tag"], vec![])],
220 options: PluginOptions {
221 operating_currencies: vec!["USD".to_string()],
222 title: None,
223 },
224 config: None,
225 };
226
227 let input_dirs = input.directives.clone();
228 let output = plugin.process(input);
229 assert_eq!(output.errors.len(), 0);
230 let directives = materialize_ops(&input_dirs, &output);
231
232 if let DirectiveData::Transaction(txn) = &directives[0].data {
233 assert!(txn.metadata.is_empty());
235 } else {
236 panic!("Expected transaction");
237 }
238 }
239}