Skip to main content

rustledger_plugin/native/plugins/
close_tree.rs

1//! Close descendant accounts automatically.
2
3use crate::types::{
4    CloseData, DirectiveData, DirectiveWrapper, PluginInput, PluginOp, PluginOutput,
5};
6
7use super::super::NativePlugin;
8
9/// Plugin that closes all descendant accounts when a parent account closes.
10///
11/// When an account like `Assets:Bank` is closed, this plugin also generates
12/// close directives for all sub-accounts like `Assets:Bank:Checking`.
13pub struct CloseTreePlugin;
14
15impl NativePlugin for CloseTreePlugin {
16    fn name(&self) -> &'static str {
17        "close_tree"
18    }
19
20    fn description(&self) -> &'static str {
21        "Close descendant accounts automatically"
22    }
23
24    fn process(&self, input: PluginInput) -> PluginOutput {
25        use std::collections::HashSet;
26
27        // Collect all accounts that are used
28        let mut all_accounts: HashSet<String> = HashSet::new();
29        for wrapper in &input.directives {
30            if let DirectiveData::Open(data) = &wrapper.data {
31                all_accounts.insert(data.account.clone());
32            }
33            if let DirectiveData::Transaction(txn) = &wrapper.data {
34                for posting in &txn.postings {
35                    all_accounts.insert(posting.account.clone());
36                }
37            }
38        }
39
40        // Collect accounts that are explicitly closed
41        let mut closed_parents: Vec<(String, String)> = Vec::new(); // (account, date)
42        for wrapper in &input.directives {
43            if let DirectiveData::Close(data) = &wrapper.data {
44                closed_parents.push((data.account.clone(), wrapper.date.clone()));
45            }
46        }
47
48        // Collect accounts that are already closed in input.
49        let mut already_closed: HashSet<String> = HashSet::new();
50        for wrapper in &input.directives {
51            if let DirectiveData::Close(data) = &wrapper.data {
52                already_closed.insert(data.account.clone());
53            }
54        }
55
56        // Start with Keep ops for every input directive.
57        let mut ops: Vec<PluginOp> = (0..input.directives.len()).map(PluginOp::Keep).collect();
58
59        // Track close directives we will insert so the same descendant
60        // doesn't get inserted twice when multiple parent prefixes apply.
61        let mut inserted_closes: HashSet<String> = HashSet::new();
62
63        for (parent, close_date) in &closed_parents {
64            let prefix = format!("{parent}:");
65            for account in &all_accounts {
66                if account.starts_with(&prefix)
67                    && !already_closed.contains(account)
68                    && !inserted_closes.contains(account)
69                {
70                    inserted_closes.insert(account.clone());
71                    ops.push(PluginOp::Insert(DirectiveWrapper {
72                        directive_type: "close".to_string(),
73                        date: close_date.clone(),
74                        filename: None, // Plugin-generated
75                        lineno: None,
76                        data: DirectiveData::Close(CloseData {
77                            account: account.clone(),
78                            metadata: vec![],
79                        }),
80                    }));
81                }
82            }
83        }
84
85        // Final ordering is the loader's responsibility — it re-sorts
86        // directives after the plugin pass.
87        PluginOutput {
88            ops,
89            errors: Vec::new(),
90        }
91    }
92}