Skip to main content

rustledger_plugin/native/plugins/
check_closing.rs

1//! Zero balance assertion on account closing.
2
3use crate::types::{
4    AmountData, BalanceData, DirectiveData, DirectiveWrapper, MetaValueData, PluginInput,
5    PluginOutput, sort_directives,
6};
7
8use super::super::NativePlugin;
9use super::utils::increment_date;
10
11/// Plugin that inserts zero balance assertion when posting has `closing: TRUE` metadata.
12///
13/// When a posting has metadata `closing: TRUE`, this plugin adds a balance assertion
14/// for that account with zero balance on the next day.
15pub struct CheckClosingPlugin;
16
17impl NativePlugin for CheckClosingPlugin {
18    fn name(&self) -> &'static str {
19        "check_closing"
20    }
21
22    fn description(&self) -> &'static str {
23        "Zero balance assertion on account closing"
24    }
25
26    fn process(&self, input: PluginInput) -> PluginOutput {
27        let mut new_directives: Vec<DirectiveWrapper> = Vec::new();
28
29        for wrapper in &input.directives {
30            new_directives.push(wrapper.clone());
31
32            if let DirectiveData::Transaction(txn) = &wrapper.data {
33                for posting in &txn.postings {
34                    // Check for closing: TRUE metadata
35                    let has_closing = posting.metadata.iter().any(|(key, val)| {
36                        key == "closing" && matches!(val, MetaValueData::Bool(true))
37                    });
38
39                    if has_closing {
40                        // Parse the date and add one day
41                        if let Some(next_date) = increment_date(&wrapper.date) {
42                            // Get the currency from the posting
43                            let currency = posting
44                                .units
45                                .as_ref()
46                                .map_or_else(|| "USD".to_string(), |u| u.currency.clone());
47
48                            // Add zero balance assertion
49                            new_directives.push(DirectiveWrapper {
50                                directive_type: "balance".to_string(),
51                                date: next_date,
52                                filename: None, // Plugin-generated
53                                lineno: None,
54                                data: DirectiveData::Balance(BalanceData {
55                                    account: posting.account.clone(),
56                                    amount: AmountData {
57                                        number: "0".to_string(),
58                                        currency,
59                                    },
60                                    tolerance: None,
61                                    metadata: vec![],
62                                }),
63                            });
64                        }
65                    }
66                }
67            }
68        }
69
70        // Sort using beancount's standard ordering
71        sort_directives(&mut new_directives);
72
73        PluginOutput {
74            directives: new_directives,
75            errors: Vec::new(),
76        }
77    }
78}