rustledger_plugin/native/plugins/
sell_gains.rs1use crate::types::{DirectiveData, PluginError, PluginInput, PluginOutput};
4
5use super::super::NativePlugin;
6
7pub struct SellGainsPlugin;
12
13impl NativePlugin for SellGainsPlugin {
14 fn name(&self) -> &'static str {
15 "sellgains"
16 }
17
18 fn description(&self) -> &'static str {
19 "Cross-check capital gains against sales"
20 }
21
22 fn process(&self, input: PluginInput) -> PluginOutput {
23 use rust_decimal::Decimal;
24 use std::str::FromStr;
25
26 let mut errors = Vec::new();
27
28 for wrapper in &input.directives {
29 if let DirectiveData::Transaction(txn) = &wrapper.data {
30 for posting in &txn.postings {
32 if let (Some(units), Some(cost), Some(price)) =
33 (&posting.units, &posting.cost, &posting.price)
34 {
35 let units_num = Decimal::from_str(&units.number).unwrap_or_default();
37 if units_num >= Decimal::ZERO {
38 continue;
39 }
40
41 let cost_per = cost
43 .number_per
44 .as_ref()
45 .and_then(|s| Decimal::from_str(s).ok())
46 .unwrap_or_default();
47
48 let sale_price = price
50 .amount
51 .as_ref()
52 .and_then(|a| Decimal::from_str(&a.number).ok())
53 .unwrap_or_default();
54
55 let expected_gain = (sale_price - cost_per) * units_num.abs();
57
58 let has_gain_posting = txn.postings.iter().any(|p| {
60 p.account.starts_with("Income:") || p.account.starts_with("Expenses:")
61 });
62
63 if expected_gain != Decimal::ZERO && !has_gain_posting {
64 errors.push(PluginError::warning(format!(
65 "Sale of {} {} at {} (cost {}) has expected gain/loss of {} but no Income/Expenses posting",
66 units_num.abs(),
67 units.currency,
68 sale_price,
69 cost_per,
70 expected_gain
71 )));
72 }
73 }
74 }
75 }
76 }
77
78 PluginOutput {
79 directives: input.directives,
80 errors,
81 }
82 }
83}