use crate::types::{DirectiveData, PluginError, PluginInput, PluginOutput};
use super::super::NativePlugin;
pub struct UnrealizedPlugin {
pub gains_account: String,
}
impl UnrealizedPlugin {
pub fn new() -> Self {
Self {
gains_account: "Income:Unrealized".to_string(),
}
}
pub const fn with_account(account: String) -> Self {
Self {
gains_account: account,
}
}
}
impl Default for UnrealizedPlugin {
fn default() -> Self {
Self::new()
}
}
impl NativePlugin for UnrealizedPlugin {
fn name(&self) -> &'static str {
"unrealized"
}
fn description(&self) -> &'static str {
"Calculate unrealized gains/losses"
}
fn process(&self, input: PluginInput) -> PluginOutput {
use rust_decimal::Decimal;
use std::collections::HashMap;
use std::str::FromStr;
let mut prices: HashMap<(String, String), (String, Decimal)> = HashMap::new();
for wrapper in &input.directives {
if let DirectiveData::Price(price) = &wrapper.data {
let key = (price.currency.clone(), price.amount.currency.clone());
let price_val = Decimal::from_str(&price.amount.number).unwrap_or_default();
if let Some((existing_date, _)) = prices.get(&key) {
if &wrapper.date > existing_date {
prices.insert(key, (wrapper.date.clone(), price_val));
}
} else {
prices.insert(key, (wrapper.date.clone(), price_val));
}
}
}
let mut positions: HashMap<String, HashMap<String, (Decimal, Decimal)>> = HashMap::new();
let mut errors = Vec::new();
for wrapper in &input.directives {
if let DirectiveData::Transaction(txn) = &wrapper.data {
for posting in &txn.postings {
if let Some(units) = &posting.units {
let units_num = Decimal::from_str(&units.number).unwrap_or_default();
let cost_basis = if let Some(cost) = &posting.cost {
cost.number_per
.as_ref()
.and_then(|s| Decimal::from_str(s).ok())
.unwrap_or_default()
* units_num.abs()
} else {
Decimal::ZERO
};
let account_positions =
positions.entry(posting.account.clone()).or_default();
let (existing_units, existing_cost) = account_positions
.entry(units.currency.clone())
.or_insert((Decimal::ZERO, Decimal::ZERO));
*existing_units += units_num;
*existing_cost += cost_basis;
}
}
}
}
for (account, currencies) in &positions {
for (currency, (units, cost_basis)) in currencies {
if *units == Decimal::ZERO {
continue;
}
if let Some((_, market_price)) = prices.get(&(currency.clone(), "USD".to_string()))
{
let market_value = *units * market_price;
let unrealized_gain = market_value - cost_basis;
if unrealized_gain.abs() > Decimal::new(1, 2) {
errors.push(PluginError::warning(format!(
"Unrealized gain on {units} {currency} in {account}: {unrealized_gain} USD"
)));
}
}
}
}
PluginOutput {
directives: input.directives,
errors,
}
}
}