Skip to main content

rustledger_plugin/native/plugins/
coherent_cost.rs

1//! Enforce cost OR price (not both) consistency.
2
3use crate::types::{DirectiveData, PluginError, PluginInput, PluginOutput};
4
5use super::super::NativePlugin;
6
7/// Plugin that ensures currencies use cost OR price consistently, never both.
8///
9/// If a currency is used with cost notation `{...}`, it should not also be used
10/// with price notation `@` in the same ledger, as this can lead to inconsistencies.
11pub struct CoherentCostPlugin;
12
13impl NativePlugin for CoherentCostPlugin {
14    fn name(&self) -> &'static str {
15        "coherent_cost"
16    }
17
18    fn description(&self) -> &'static str {
19        "Enforce cost OR price (not both) consistency"
20    }
21
22    fn process(&self, input: PluginInput) -> PluginOutput {
23        use std::collections::{HashMap, HashSet};
24
25        // Track which currencies are used with cost vs price
26        let mut currencies_with_cost: HashSet<String> = HashSet::new();
27        let mut currencies_with_price: HashSet<String> = HashSet::new();
28        let mut first_use: HashMap<String, (String, String)> = HashMap::new(); // currency -> (type, date)
29
30        for wrapper in &input.directives {
31            if let DirectiveData::Transaction(txn) = &wrapper.data {
32                for posting in &txn.postings {
33                    if let Some(units) = &posting.units {
34                        let currency = &units.currency;
35
36                        if posting.cost.is_some() && !currencies_with_cost.contains(currency) {
37                            currencies_with_cost.insert(currency.clone());
38                            first_use
39                                .entry(currency.clone())
40                                .or_insert(("cost".to_string(), wrapper.date.clone()));
41                        }
42
43                        if posting.price.is_some() && !currencies_with_price.contains(currency) {
44                            currencies_with_price.insert(currency.clone());
45                            first_use
46                                .entry(currency.clone())
47                                .or_insert(("price".to_string(), wrapper.date.clone()));
48                        }
49                    }
50                }
51            }
52        }
53
54        // Find currencies used with both
55        let mut errors = Vec::new();
56        for currency in currencies_with_cost.intersection(&currencies_with_price) {
57            errors.push(PluginError::error(format!(
58                "Currency '{currency}' is used with both cost and price notation - this may cause inconsistencies"
59            )));
60        }
61
62        PluginOutput {
63            directives: input.directives,
64            errors,
65        }
66    }
67}