rustledger_plugin/native/plugins/coherent_cost.rs
1//! Enforce consistent cost tracking per currency.
2
3use crate::types::{DirectiveData, PluginError, PluginInput, PluginOutput};
4
5use super::super::NativePlugin;
6
7/// Plugin that ensures currencies are tracked consistently with cost or price-only.
8///
9/// If a currency is used with cost notation `{...}` in some postings, it should
10/// not be used with price-only notation `@` (without cost) in other postings,
11/// as this indicates inconsistent tracking.
12///
13/// Note: Having BOTH cost AND price on the same posting is valid and common
14/// when selling positions (cost = acquisition price, price = sale price).
15pub struct CoherentCostPlugin;
16
17impl NativePlugin for CoherentCostPlugin {
18 fn name(&self) -> &'static str {
19 "coherent_cost"
20 }
21
22 fn description(&self) -> &'static str {
23 "Enforce consistent cost tracking per currency"
24 }
25
26 fn process(&self, input: PluginInput) -> PluginOutput {
27 use std::collections::HashSet;
28
29 // Track currencies used with cost (with or without price)
30 // Use references to avoid cloning currency strings
31 let mut currencies_with_cost: HashSet<&str> = HashSet::new();
32 // Track currencies used with price-only (no cost)
33 let mut currencies_with_price_only: HashSet<&str> = HashSet::new();
34
35 for wrapper in &input.directives {
36 if let DirectiveData::Transaction(txn) = &wrapper.data {
37 for posting in &txn.postings {
38 if let Some(units) = &posting.units {
39 let currency = units.currency.as_str();
40
41 // Check if this posting has cost
42 if posting.cost.is_some() {
43 currencies_with_cost.insert(currency);
44 } else if posting.price.is_some() {
45 // Price-only (no cost) - this is the problematic case
46 currencies_with_price_only.insert(currency);
47 }
48 }
49 }
50 }
51 }
52
53 // Find currencies used with cost in some places and price-only in others
54 // Collect and sort for deterministic error ordering
55 let mut inconsistent: Vec<_> = currencies_with_cost
56 .intersection(¤cies_with_price_only)
57 .copied()
58 .collect();
59 inconsistent.sort_unstable();
60
61 let errors: Vec<_> = inconsistent
62 .into_iter()
63 .map(|currency| {
64 PluginError::error(format!(
65 "Currency '{currency}' is used with both cost and price-only notation - this may cause inconsistencies"
66 ))
67 })
68 .collect();
69
70 PluginOutput {
71 directives: input.directives,
72 errors,
73 }
74 }
75}