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, PluginOutput, sort_directives,
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        // Find child accounts for each closed parent
49        let mut new_directives = input.directives;
50
51        for (parent, close_date) in &closed_parents {
52            let prefix = format!("{parent}:");
53            for account in &all_accounts {
54                if account.starts_with(&prefix) {
55                    // Check if already closed
56                    let already_closed = new_directives.iter().any(|w| {
57                        if let DirectiveData::Close(data) = &w.data {
58                            &data.account == account
59                        } else {
60                            false
61                        }
62                    });
63
64                    if !already_closed {
65                        new_directives.push(DirectiveWrapper {
66                            directive_type: "close".to_string(),
67                            date: close_date.clone(),
68                            filename: None, // Plugin-generated
69                            lineno: None,
70                            data: DirectiveData::Close(CloseData {
71                                account: account.clone(),
72                                metadata: vec![],
73                            }),
74                        });
75                    }
76                }
77            }
78        }
79
80        // Sort using beancount's standard ordering
81        sort_directives(&mut new_directives);
82
83        PluginOutput {
84            directives: new_directives,
85            errors: Vec::new(),
86        }
87    }
88}