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